2026年1月

这个开源项目精选了超级多的 skills,从文档处理,工具调用,市场分析,数据分析,系统安全等 各大精选的 skill 都有。


📌 转载信息
转载时间:
2026/1/15 18:29:29

如果你不想看废话,请直接滚动到最后面,有两个 Pure 直接使用的方案。

Kiro 是什么

Kiro 是亚马逊云科技于 2025 年 7 月 16 日推出的专为 AI Agent 设计的集成开发环境(agentic IDE)。

新注册账号有 500 积分可以使用高级模型。

原理

把 kiro 账号 添加到 KiroGate ,转为 标准的 API,配置到 CodeSwitch , 可以在 claude codex 使用

前置准备

各软件配置

Kiro Account Manager 的配置

为什么要这个软件》》》》》最主要的原因是自动刷新 token,免得账号过期。

支持多种方式添加账号

  • 添加账号

KiroGate 配置

安装

GitHub - aliom-v/KiroGate: OpenAI & Anthropic 兼容的 Kiro IDE API 代理网关,支持 Claude Code CLI

  docker run -d -p 8000:8000
  -v kirogate_data:/app/data
  -e PROXY_API_KEY=
  -e ADMIN_PASSWORD="aadf5beb"
  -e USER_SESSION_SECRET=
  -e ADMIN_SECRET_KEY=
  --name kirogate  ghcr.io/awei84/kirogate:main

配置 Kirogate

注册普通用户

因为只有普通用户才能添加自己的 token


访问后台,审核账号

注意 管理员的密码 是 启动容器使用配置的 ADMIN_PASSWORD

KirGate 提供的是隐藏的管理后台,需要手动输入路由进入

比如:http://127.0.0.1:8000/admin/login

/admin/login  → 登录页面
/admin        → 管理面板(需登录)
/admin/logout → 退出登录

切换普通账号,添加 token

imageimage

这个 token 可以在 Kiro Account Manager 复制

添加 token 的时候 可以选择公开或者私有

使用

创建 API key

配置到 CodeSwitch

Kiro 的服务地址:ip:8000/v1/chat/completions

直接配置到 Claude 或者 codex

# OpenAI 格式
curl http://localhost:8000/v1/chat/completions \
  -H "Authorization: Bearer sk-your-api-key" \
  -H "Content-Type: application/json" \
  -d '{"model": "claude-sonnet-4-5", "messages": [{"role": "user", "content": "你好"}]}'

# Anthropic 格式
curl http://localhost:8000/v1/messages \
  -H "x-api-key: sk-your-api-key" \
  -H "Content-Type: application/json" \
  -d '{"model": "claude-sonnet-4-5", "max_tokens": 1024, "messages": [{"role": "user", "content": "你好"}]}' 

号池是啥

你家一个 token 我家一个 token,就变成了 token 池子了

如何获取账号

  • 方法一:google 直接登录
  • 方法二:github 直接登录
  • 方法三:qq 注册 github,然后继续方法二

偷懒方案 1

可以用下面两个现成的。

偷懒方案 2

安装这个软件 GitHub - awei84/KiroGate: issues pr 请去上游仓库

🔐
获取 Refresh Token
🌐 方式一:浏览器获取(推荐)

1打开 https://app.kiro.dev/account/usage 并登录 2F12 打开开发者工具
3点击 应用/Application → 存储/Storage  Cookie 4选择 https://app.kiro.dev 5复制 RefreshToken 的值

🛠️ 方式二:Kiro Account Manager

使用 Kiro Account Manager 可以轻松管理多个账号的 Refresh Token 

参考


📌 转载信息
原作者:
dream_bugless
转载时间:
2026/1/15 18:28:31

昨晚开始开始入手 OpenCode ,整理了一份从零开始的安装与配置笔记,分享给各位佬友。

第一阶段:基础安装与核心插件

1. 安装 OpenCode

推荐使用 brew 安装,稳定性更高:

  • macOS/Linux: brew install anomalyco/tap/opencode
  • Node 环境: npm i -g opencode-ai

2. 必装 “全家桶” 插件

安装完成后先输入 opencode 启动(能白嫖 GLM4.7),然后在会话中直接粘贴以下链接安装 oh-my-opencode

Install and configure by following the instructions here https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/refs/heads/master/README.md

进阶推荐:

  • opencode-dcp: 自动清理对话历史中过时的工具输出,显著减少 Token 消耗。

第二阶段:进阶配置(接入 OneAPI / 中转站)

由于 OpenCode CLI 执行任务时会有大量 Tool Use 调用,普通 URL 转发容易协议报错。这里推荐使用 CLIProxyAPI (CPA) 进行协议转换。

1. 部署 CPA 环境

建议直接走 Source Build,日志更透明:

git clone https://github.com/router-for-me/CLIProxyAPI.git
cd CLIProxyAPI
go mod download
go run main.go # 启动 

2. CPA 核心配置 (config.yaml)

坑点: 字段名必须准确,否则会静默失败。

  • 确保使用 auth-dir 而不是旧版的 credentials-directory
  • 供应商字段是 openai-api-keys(复数,带 s)。
port: 8317 auth-dir: "/绝对路径/auth" allow-unauthenticated: true # 本地调试建议开启 openai-api-keys: - api-key: "sk-OneAPI令牌" base-url: "https://OneAPI地址/v1" models: - id: "claude-opus-4-5" # OpenCode 中显示的名称 map-to: "claude-opus-4-5-20251101" # OneAPI 后台真实 ID 

第三阶段:OpenCode 配置文件打通

修改 ~/.config/opencode/opencode.json。因为 CPA 侧开了免密,这里直接配置 Provider 即可:

{
  "$schema": "https://opencode.ai/config.json",
  "plugin": [
    "oh-my-opencode",
    "@tarquinen/opencode-dcp@latest"
  ],
  "provider": {
    "anthropic": {
      "options": {
        "baseURL": "http://127.0.0.1:8317/v1"
      }
    }
  }
}

踩坑

  1. 代理污染(502 报错):如果终端开了 http_proxy,请求 localhost 可能会被劫持导致失败。执行前记得:unset http_proxy https_proxy all_proxy 或者使用 curl -v --noproxy “*” http://127.0.0.1:8317/v1/models 测试连通性。
  2. Thinking 模式没显示?:按 Ctrl + P,搜索 think 即可手动开启或关闭思维链显示。
  3. YAML 解析失败:Go 解析路径时对~/ 支持不佳,建议在 config.yaml 中全部使用绝对路径。

管理端 UI


📌 转载信息
转载时间:
2026/1/15 18:27:44

此前,我曾在《[教程] 如何使用 AI 智能规划你的专属行程?》一文中分享过基于 MCP 智能生成旅游攻略的方案。当时的解决方案主要依赖 “厚重” 的提示词(Prompt)来驱动 Agent。这种方式虽然可行,但存在一个显著弊端:大量的 Context(上下文)窗口被提示词本身占用,导致实际处理任务的上下文空间被浪费,且 Token 消耗巨大。
为了解决上述问题,在深入研究了 SKILL 机制后,我调整了技术思路,采用了 SKILL + MCP 的组合架构。通过将复杂的指令逻辑封装为 SKILL,减轻了 Prompt 的负担,从而释放了更多的上下文空间给实际业务数据。
经过测试,在新架构下生成一篇简单的旅游攻略,Token 消耗成功控制在了 169.7k 左右,相比纯 Prompt 驱动方案有了显著优化。
目前该方案的 SKILL 实现已上传至 GitHub,欢迎参考: SKILL 地址: QianJue-CN/TravePlanHelper
当然,目前的 SKILL 实现尚不完善,对于上下文的精细控制和 Token 消耗的极致优化也仅仅是一个开始。本文旨在抛砖引玉,分享一次技术探索的尝试,希望能得到各位佬友的指正与认可。
杭州 - 千岛湖周末情侣游攻略.pdf


📌 转载信息
原作者:
QianJueOnline
转载时间:
2026/1/15 18:27:07

最近购买了一台龙芯架构的电脑,苦于没有支持龙芯架构的 ssh 软件,偶然发现了它–Finalshell。界面非常复古但是功能强大,更可贵的是似乎一直在更新,更更可贵的是居然支持国产的龙芯架构,格局一下子打开了好吗


推荐给各位佬友试试。
官网地址:
https://www.hostbuf.com/t/1081.html


📌 转载信息
转载时间:
2026/1/15 18:26:35

今天给佬们分享一个自己在玩的项目,不同于市面上现有的大部分 AI 项目给出实操投资,我们这个项目更加偏向于只是收集市场数据,给出盘前盘后的看法,方便大家有个基础的参考。

技术栈:
python + TypesScript

数据来源:
Tavily + AKSHARE

AI: GEMINI 2.5pro

仪表盘:查看一些盘面大数据


基金池:添加我们关心的基金,可以配置定时任务生成盘前盘后的数据



股票:和上面的基金池一样

情绪:主要是针对市场的数据进行复盘,风格比较喷子


情报:就是展示我们生成的盘前 / 盘后的报表

商品:目前只实现了针对黄金和白银分析

系统配置:就是我们配置 AI 和 Tavily 联网搜索的页面

目前仍在更新…
仓库地址:GitHub - Austin-Patrician/eastmoney


📌 转载信息
原作者:
austin_zhang
转载时间:
2026/1/15 18:26:30

[Scriptable] NASA 每日天文图 (APOD) iOS 小组件

一个在 iPhone 桌面上看 NASA 的每日天文一图 (APOD)。

核心亮点

零门槛 (Zero Config):不需要申请 NASA API Key。直接抓取 NASA 官网数据,省去注册麻烦,也不用担心 Key 额度超限。
自动取色 (Dynamic Color):脚本会自动提取当日图片的主色调,生成磨砂质感的渐变背景蒙版。
哪怕图片是黑白的,组件也不会单调。
超强抗网络波动:
智能选图:自动识别官网主图,排除 Logo 和图标。
多重兜底:直连 NASA 失败时,自动切换到 weserv 图片代理,大幅提升国内加载成功率。
缓存优先:断网或请求失败时,自动展示上一次缓存的图片,绝不开天窗(不黑屏)。
UI 细节:
底部半透明信息卡片,模拟 iOS 原生组件质感。
顶部自动根据主色调生成 “APOD” 胶囊标签。
遇到 “视频日”(当天是视频没图片),会自动显示提示,并保持美观的背景。
预览图


使用方法

App Store 下载 Scriptable。
打开 App,点击右上角 + 号,新建脚本。
将下方代码完整复制粘贴进去,命名为 NASA APOD。
回到桌面,添加 Scriptable 小组件,尺寸选 中号 (Medium) 或 大号 (Large)。
在小组件设置里,Script 选择刚才保存的脚本即可。
脚本代码

// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: deep-blue; icon-glyph: star;

/*
 * APOD 零 Key 美化稳定版(不显示在线状态)
 * - 数据源:https://apod.nasa.gov/apod/astropix.html
 * - 主图优先:href="image/..." -> img src="image/..."
 * - 图片兜底:直连失败 -> weserv 代理
 * - 防黑缓存:缓存过小图会自动丢弃
 */

const LOCALE = "zh-CN";
const HTML_TIMEOUT_SEC = 25;
const IMAGE_TIMEOUT_SEC = 45;
const PREVIEW_SIZE = "medium"; // small | medium | large

// 如果你之前已经黑了很多次,强烈建议把它改成 true 跑一次,再改回 false
const RESET_CACHE_ONCE = false;

const fm = FileManager.local();
const cacheDir = fm.joinPath(fm.documentsDirectory(), "apod_nokey_widget_cache");
const cacheJsonPath = fm.joinPath(cacheDir, "apod.json");
const cacheImgPath = fm.joinPath(cacheDir, "apod.jpg");

function ensureCacheDir() {
  if (!fm.fileExists(cacheDir)) fm.createDirectory(cacheDir);
}

function resetCacheIfNeeded() {
  if (!RESET_CACHE_ONCE) return;
  try { if (fm.fileExists(cacheJsonPath)) fm.remove(cacheJsonPath); } catch {}
  try { if (fm.fileExists(cacheImgPath)) fm.remove(cacheImgPath); } catch {}
}

async function requestText(url, timeoutSec) {
  const req = new Request(url);
  req.timeoutInterval = timeoutSec;
  const text = await req.loadString();
  return { text, statusCode: req.response?.statusCode };
}

async function requestImage(url, timeoutSec) {
  const req = new Request(url);
  req.timeoutInterval = timeoutSec;
  const img = await req.loadImage();
  return img;
}

function readCache() {
  try {
    if (!fm.fileExists(cacheJsonPath)) return null;
    const wrapper = JSON.parse(fm.readString(cacheJsonPath));
    const img = fm.fileExists(cacheImgPath) ? fm.readImage(cacheImgPath) : null;
    return { data: wrapper.data, img };
  } catch {
    return null;
  }
}

function writeCache(data, img) {
  ensureCacheDir();
  fm.writeString(cacheJsonPath, JSON.stringify({ savedAt: Date.now(), data }));
  if (img) fm.writeImage(cacheImgPath, img);
}

function stripHtml(s) {
  return String(s || "").replace(/<[^>]*>/g, "").replace(/\s+/g, " ").trim();
}

function absApodUrl(pathOrUrl) {
  if (!pathOrUrl) return null;
  const base = "https://apod.nasa.gov/apod/";
  const raw = String(pathOrUrl).trim();

  if (/^https?:\/\//i.test(raw)) {
    // 强制 apod 走 https
    return raw.replace(/^http:\/\/apod\.nasa\.gov\//i, "https://apod.nasa.gov/");
  }
  return (base + raw.replace(/^\//, "")).replace(/^http:\/\/apod\.nasa\.gov\//i, "https://apod.nasa.gov/");
}

function pickMainImageUrl(html) {
  // 1) 最稳:主图通常包在 <a href="image/...jpg"><img ...></a>
  const href = html.match(/<a[^>]+href="(image\/[^"]+\.(?:jpg|jpeg|png|webp)(?:\?[^"]*)?)"/i);
  if (href?.[1]) return absApodUrl(href[1]);

  // 2) 次稳:直接 img src="image/..."
  const src = html.match(/<img[^>]+src="(image\/[^"]+\.(?:jpg|jpeg|png|webp)(?:\?[^"]*)?)"/i);
  if (src?.[1]) return absApodUrl(src[1]);

  // 3) 兜底:找任意 image/xxx.jpg
  const any = html.match(/(image\/[^\s"'<>]+\.(?:jpg|jpeg|png|webp))/i);
  if (any?.[1]) return absApodUrl(any[1]);

  return null;
}

function parseApodHtml(html) {
  const bolds = [...html.matchAll(/<b>([\s\S]*?)<\/b>/gi)].map((m) => stripHtml(m[1]));
  const title =
    bolds.find(
      (t) =>
        t &&
        !/Astronomy Picture of the Day/i.test(t) &&
        !/APOD/i.test(t) &&
        t.length >= 3 &&
        t.length <= 90
    ) || "NASA APOD";

  const dateMatch = html.match(/(\d{4}\s+[A-Za-z]+\s+\d{1,2})/);
  const dateText = dateMatch ? dateMatch[1] : "";

  let credit = "";
  const creditMatch = html.match(/Image Credit[^<]*<\/b>\s*([\s\S]*?)<\/center>/i);
  if (creditMatch?.[1]) credit = stripHtml(creditMatch[1]).slice(0, 120);

  const isVideo = /<iframe\b/i.test(html) || /youtube\.com|youtu\.be|vimeo\.com/i.test(html);

  const imageUrl = pickMainImageUrl(html);

  return { title, dateText, credit, imageUrl, isVideo };
}

function parseToLocalDateString(maybeEnglishDate) {
  if (!maybeEnglishDate) return "";
  try {
    const d = new Date(maybeEnglishDate);
    if (!isNaN(d.getTime())) {
      return d.toLocaleDateString(LOCALE, { year: "numeric", month: "long", day: "numeric" });
    }
  } catch {}
  return String(maybeEnglishDate);
}

function weservProxy(url) {
  const stripped = String(url).replace(/^https?:\/\//, "");
  return `https://images.weserv.nl/?url=${encodeURIComponent(stripped)}`;
}

function isImageUsable(img) {
  if (!img) return false;
  // Scriptable 的 Image 通常有 size
  const w = img.size?.width || 0;
  const h = img.size?.height || 0;
  // 主图不可能这么小;避免 logo/透明小图导致“黑”
  return w >= 300 && h >= 300;
}

async function downloadMainImage(imageUrl) {
  const direct = imageUrl;
  const proxy = weservProxy(imageUrl);

  try {
    const img = await requestImage(direct, IMAGE_TIMEOUT_SEC);
    if (isImageUsable(img)) return img;
  } catch {}

  const img2 = await requestImage(proxy, IMAGE_TIMEOUT_SEC);
  if (!isImageUsable(img2)) throw new Error("下载到的图片过小(可能不是主图)");
  return img2;
}

function makeOverlayGradient() {
  // 不要太黑:让背景图清晰可见,同时保证底部文字清楚
  const g = new LinearGradient();
  g.locations = [0, 0.55, 1];
  g.colors = [
    new Color("#000000", 0.16),
    new Color("#000000", 0.06),
    new Color("#000000", 0.46),
  ];
  return g;
}

function makeFallbackGradient() {
  const g = new LinearGradient();
  g.locations = [0, 1];
  g.colors = [new Color("#0b1220"), new Color("#111827")];
  return g;
}

function addBadge(container) {
  const pill = container.addStack();
  pill.setPadding(6, 10, 6, 10);
  pill.cornerRadius = 999;
  pill.backgroundColor = new Color("#0b1020", 0.30);

  const t = pill.addText("APOD");
  t.textColor = new Color("#ffffff", 0.92);
  t.font = Font.semiboldSystemFont(10);
}

function addInfoCard(container, data, family) {
  const card = container.addStack();
  card.layoutVertically();
  card.setPadding(12, 12, 12, 12);
  card.cornerRadius = 16;
  card.backgroundColor = new Color("#0b1020", 0.36);

  const titleFont = family === "small" ? 15 : 17;
  const dateFont = family === "small" ? 11 : 12;

  const title = card.addText(data?.title || "NASA APOD");
  title.textColor = Color.white();
  title.font = Font.boldSystemFont(titleFont);
  title.lineLimit = 2;

  card.addSpacer(6);

  const dateLine = parseToLocalDateString(data?.dateText || "");
  if (dateLine) {
    const d = card.addText(dateLine);
    d.textColor = new Color("#e5e7eb", 0.92);
    d.font = Font.systemFont(dateFont);
    d.lineLimit = 1;
  }

  if (data?.credit) {
    const c = card.addText(`© ${data.credit}`);
    c.textColor = new Color("#cbd5e1", 0.85);
    c.font = Font.systemFont(10);
    c.lineLimit = 1;
  }

  if (!data?.imageUrl && data?.isVideo) {
    card.addSpacer(6);
    const v = card.addText("今日为视频,官网无可用图片预览");
    v.textColor = new Color("#cbd5e1", 0.9);
    v.font = Font.systemFont(10);
    v.lineLimit = 2;
  }
}

async function createWidget() {
  resetCacheIfNeeded();

  const cache = readCache();
  let data = cache?.data || null;
  let bgImg = isImageUsable(cache?.img) ? cache.img : null;

  // 在线抓取
  try {
    const url = "https://apod.nasa.gov/apod/astropix.html";
    const { text, statusCode } = await requestText(url, HTML_TIMEOUT_SEC);
    if (statusCode && statusCode >= 400) throw new Error(`HTTP ${statusCode}`);

    const parsed = parseApodHtml(text);
    data = parsed;

    if (parsed.imageUrl) {
      const img = await downloadMainImage(parsed.imageUrl);
      bgImg = img;
      writeCache(parsed, img);
    } else {
      // 只缓存文字,不覆盖旧图
      ensureCacheDir();
      fm.writeString(cacheJsonPath, JSON.stringify({ savedAt: Date.now(), data: parsed }));
    }
  } catch (e) {
    // 在线失败:无缓存才显示错误页
    if (!data && !bgImg) {
      const w = new ListWidget();
      w.setPadding(16, 16, 16, 16);
      w.backgroundGradient = makeFallbackGradient();
      const t = w.addText("APOD 暂不可用");
      t.textColor = Color.white();
      t.font = Font.boldSystemFont(16);
      w.addSpacer(8);
      const m = w.addText(String(e.message).slice(0, 180));
      m.textColor = new Color("#ffcccc");
      m.font = Font.systemFont(11);
      m.lineLimit = 4;
      w.url = "https://apod.nasa.gov/apod/astropix.html";
      return w;
    }
  }

  const w = new ListWidget();
  w.setPadding(0, 0, 0, 0);
  w.refreshAfterDate = new Date(Date.now() + 60 * 60 * 1000);
  w.url = "https://apod.nasa.gov/apod/astropix.html";

  if (bgImg) {
    w.backgroundImage = bgImg;
    w.backgroundGradient = makeOverlayGradient();
  } else {
    w.backgroundGradient = makeFallbackGradient();
  }

  const family = config.widgetFamily || "medium";
  const content = w.addStack();
  content.layoutVertically();
  content.setPadding(14, 14, 14, 14);

  addBadge(content);
  content.addSpacer();
  addInfoCard(content, data, family);

  return w;
}

// 运行
const widget = await createWidget();
if (config.runsInWidget) {
  Script.setWidget(widget);
} else {
  if (PREVIEW_SIZE === "small") widget.presentSmall();
  else if (PREVIEW_SIZE === "large") widget.presentLarge();
  else widget.presentMedium();
}
Script.complete();

📌 转载信息
原作者:
user321
转载时间:
2026/1/15 18:24:48

在阅读完成 https://linux.do/t/topic/1371904 的内容后,发现自己想要的不是 MCP 服务,而是 API 接口,然后就自己动手修改了一个 API 的版本。在前辈的功能基础上,增加了生图的支持。

个人目前是:配合 NAS+Cloudflared 做内网穿透使用,直接变成了随便联网使用的接口。

项目地址是:GitHub - CloudRobot/perplexity-ai: Unofficial API Wrapper for Perplexity.ai + Account Generator with Web Interface

轻喷。


📌 转载信息
原作者:
cloudrobot
转载时间:
2026/1/15 18:24:45

LLM Agent 的训练高度依赖多样的工具交互环境。然而,真实环境访问受限 ,LLM 模拟环境容易产生幻觉和不一致 ,而人工编写沙盒又面临成本高昂、难以扩展的难题 。
针对这一难题,我们提出了 EnvScaler —— 一个通过程序合成环境的自动化框架!利用 LLM 自动编写可执行的 Python 程序,构建成百上千个不同主题的交互式环境,并自动生成配套的任务和验证逻辑。
EnvScaler 由 SkelBuilder 和 ScenGenerator 两大核心组件组成,旨在实现环境与任务的全自动构建。
环境构建 (SkelBuilder):从文本挖掘到代码实现
主题挖掘与规划:从现有文本数据中挖掘环境主题,自动规划状态空间与工具集。
程序化实现:将规划转化为完整的 Python 程序代码。
质量保证:引入双 Agent 循环质检(Dual-Agent Inspection)机制,确保生成的环境代码质量过硬。
场景构建 (ScenGenerator):基于规则的可验证奖励
数据与任务生成:为每个环境生成对应的状态数据和挑战性任务。
验证逻辑生成:我们将任务拆解为检查列表(Checklist),并将每个检查点转换为针对环境最终状态的 Python 布尔函数。这意味着 RL 训练可以获得精准的、基于规则的、可验证的 Reward 信号,彻底告别模糊的文本反馈。
规模与实测效果:
利用 EnvScaler,我们合成了 191 个环境和约 7000 个场景。
应用到 Qwen3 模型的 SFT 与 RL 训练中,在 BFCL-v3 Multi-Turn、Tau-Bench 和 ACEBench-Agent 等基准测试上均取得了显著提升!
Qwen3-4B: BFCL-MT +12.62, Tau-Bench +7.62, ACEBench-Agent +15.27
Qwen3-8B: BFCL-MT +13.00, Tau-Bench +6.62, ACEBench-Agent +12.50

数据与代码现已全面开源!
arxiv:[2601.05808] EnvScaler: Scaling Tool-Interactive Environments for LLM Agent via Programmatic Synthesis
GitHub:GitHub - RUC-NLPIR/EnvScaler: The official implementation of "EnvScaler: Scaling Tool-Interactive Environments for LLM Agent via Programmatic Synthesis".
欢迎各位佬友尝鲜!
点点 star孩子将不胜感激!!


📌 转载信息
原作者:
QingChang
转载时间:
2026/1/15 18:24:30

混了这么久社区我也是终于三级了啊 (凌晨四点升级)!这几天用 AI 糊出来了一个小玩具,可以配合站里大佬
@F-droid 的项目🎉Gemini Business 2API 来了 | 支持 Docker 一键启动! 使用 Gemini Business 的各个 gemini 模型(大香蕉随便用说是)。

所有需要用到的项目我会在帖子最后给出地址,这里需要搭建的项目为 API 反代Hugging Face 镜像(也可以换成别的,例如 zeabur)和域名邮箱

这里先给出 github 地址,具体的配置详情都在 github 中可自行查阅 希望大家可以帮我点点 star,我在这里感激不尽

GeminiForge

现在来说说项目的具体功能:
通过 github 工作流使用配置的代理节点,域名邮箱和凭证上传地址来自动注册 Gemini Business 账号,获取凭证并上传至 2api 中,可以说是相当方便了。

代理除了常见的 HTTP/SOCKS5 代理,还额外支持 VLESS 代理(塞了个 singbox,这里建议节点用好一点,不然可能打不开网页或者接不到验证码,GitHub 的 ip 无法注册)

vless 支持两种格式,一种是正常的 VLESS URL,另一种就是 YAML 配置

格式一:VLESS URL(推荐)

vless://uuid@server:port?type=tcp&security=reality&sni=example.com&fp=chrome&pbk=xxx

格式二:YAML 配置

{ server: example.com, port: 443, uuid: xxx-xxx, flow: xtls-rprx-vision, ... }

项目设定为每 6 小时自动运行一次,一次注册两个账号并上传凭证,并且支持并发注册,最多 5 并发,这些设置可以通过修改 workflows 中的 register.yml 文件修改。

最后是用到的几位大佬的帖子和项目链接,大家可以自行查看与搭建。

注册机逻辑

API 反代

Hugging Face 镜像

域名邮箱搭建


📌 转载信息
原作者:
starsdream
转载时间:
2026/1/15 18:23:41

众所周知,OpenAI、Anthropic 和 Google 三家的模型格式各不相同。目前主流是使用 基于 NewAPI 的中转站,在 OpenCode 的配置文件中通过自定义类型进行接入。

常见的配置如下:

{
  "$schema": "https://opencode.ai/config.json",
  "provider": {
    "new-api": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "NewAPI",
      "options": {
        "baseURL": "https://xxx/v1"
      },
      "models": {
        "gemini-2.0-flash": { "name": "gemini-2.0-flash" }
      }
    }
  }
}

这里存在一个潜在问题: 在这种配置下,程序实际上是在调用 /v1/chat/completions 接口。对于 Gemini 渠道的模型,请求会经过 NewAPI 的一层或多层格式转换逻辑。这不仅增加了延迟,还可能导致参数缺失或兼容性报错。

更优的解决方案: 既然部分中转站支持 Gemini 原生格式,且 OpenCode 底层基于 Vercel AI SDK,我们完全可以绕过 OpenAI 兼容层。

通过查阅 AI SDK Provider 列表,我们可以直接将 npm 包替换为原生的 @ai-sdk/google

优化后的配置:

  • 修改 npm 字段:@ai-sdk/openai-compatible 改为 @ai-sdk/google
  • 保持 baseURL 依然指向你的中转地址。
{
  "$schema": "https://opencode.ai/config.json",
  "provider": {
    "google-native": {
      "npm": "@ai-sdk/google",
      "name": "Google Native",
      "options": {
        "baseURL": "https://your-proxy.com/v1"
      },
      "models": {
        "gemini-2.5-flash": { "name": "gemini-2.5-flash" }
    },
    "anthropic-native": {
      "npm": "@ai-sdk/anthropic",
      "name": "Anthropic Native",
      "options": {
        "baseURL": "https://your-proxy.com/v1"
      },
      "models": {
        "claude-3-5-sonnet-20241022": { "name": "claude-3-5-sonnet-20241022" }
      }
    }
  }
}

这样,调用将直接走 Google 原生协议,省去了中间的转换逻辑,响应更迅速且功能支持更完整。针对 Claude 渠道,替换为 @ai-sdk/anthropic 也是同理。


📌 转载信息
转载时间:
2026/1/15 18:21:51

只是分享哈~不代表这三家真实加速状态!

同源站服务器、同程序、同缓存规则对比:

CF 优选域名

感谢站内大佬提供的 CF 优选教程及优选地址(saas.sin.fan

阿里云 ESA


腾讯云 EdgeOne


但是我的 CF 优选是用的同一个顶级域名… 不知道会不会出现报错
我是上午 10 点多部署的,到现在看目前还是没问题的
都是上午部署的~


📌 转载信息
原作者:
sumochen
转载时间:
2026/1/15 18:21:40

更新到了 1.2.0 版本

集合了四个项目

可以用两边的额度,google_search 和大香蕉生图



📌 转载信息
转载时间:
2026/1/15 18:21:10

发现一家提供免登入可用 Nano Banana 生图的站
有兴趣可以玩看看
以下是我试着生成的图片及提示词

提示词

{
“FaceReference”: {
“Mode”: “Strict face preservation”,
“Instruction”: “Use uploaded reference for exact facial features”,
“Consistency”: “Face identical across all nine frames”
},
“GridComposition”: {
“FocalLengthMix”: “35mm full-body to 85mm close-ups”,
“PoseVariety”: [
“Wide stance hands behind head”,
“Palm extended toward camera”,
“OK gesture over eye playful”,
“Chin resting in both hands”,
“Half face covered by hand”,
“Twirling with hair flowing”,
“Jumping with arms up”,
“Looking over shoulder”,
“Candid laughing”
]
},
“PersonaDetails”: {
“Subject”: {
“Type”: “Same as reference”,
“Wardrobe”: “Light beige knit crop top, high-waisted blue jeans, delicate gold necklace”,
“OverallPresence”: “Confident, radiant, approachable”
}
},
“Environment”: {
“Setting”: “Outdoor open sky”,
“Background”: “Vibrant azure sky with clouds”,
“Lighting”: {
“Style”: “Harsh high-key natural sunlight”,
“Quality”: “Crisp defined shadows”
}
},
“ImageQuality”: {
“Resolution”: “8K hyper-realistic”,
“Aesthetic”: “High-end lifestyle campaign”
},
“NegativePrompt”: [
“indoor”,
“artificial light”,
“different face”,
“altered facial features”
],
“ResponseFormat”: {
“AspectRatio”: “1:1”
}
}

偷偷说一下,目前我正在进行 APP 限免的板块申请
如果可以的话希望大家支持一下!

请进

【APP 限免】 板块申请


📌 转载信息
原作者:
josenlou
转载时间:
2026/1/15 18:20:57

前言
前面已经成功搭建了苹果 CMS 影视站,详细教程查看《苹果 CMS V10 搭建教程》。在上一篇文章末尾,留了几个问题:

1、服务器配置到底如何选择

2、如何修改当前模板的网站的 Logo

3、网站首页的封面如何设置

4、模板不好看如何安装其它模板

5、如何通过域名访问网站

接下来,将逐一回答,如果有其它问题,欢迎大家进交流群一起探讨:点击进入站长破壁者交流群

1、服务器配置到底如何选择
搭建影视站服务器如何选择,正如前面说的,这取决于后续具体应用场景,这里简单说明下需要考虑的点:

如果只是学习 / 玩,在自己本地搭建即可。

如果单纯为了,搭建一个影视站,然后能够在互联网访问,则需要服务器了,对于配置其实没什么要求,1 核 1G 也能够安装。

如果想搭建一个让很多人观看的影视站,那么对服务器配置就有一定要求了,比如:用户在大陆则买大陆的服务器比较好;用户在亚太则买香港、日本的服务器;用户在海外则推荐买美国服务器。配置建议选择不低于 2 核 4G 的服务器,通常配置越高后续程序运行越流畅,视频采集速度也更快。

这里稍微展开说明下,影视站如果需要体验观感比较好,那么线路的选择就比较重要了,建议选择三网精品线路的服务器,并且线路的带宽大小也非常重要:大陆线路的带宽通常比较小;亚太次之并且价格通常比较贵;美国的三网精品服务器带宽一般都比较大性价比较高。教程中使用的服务器是 VMRack 三网精品服务器,体验非常好。

2、如何修改当前模板的网站的 Logo
登录管理后台,在系统 -> 网站参数配置中修改网站的基本信息。


访问网站首页,发现刚才设置的 logo 并没有生效,检查代码发现,logo 是固定写死的,所以我们在宝塔面板中,重新上传 logo 即可。

在网站的模板目录下,上传 logo 图片:

上传后 / 或者修改源码:

刷新官网:

可以看到 icon 与 logo 都更新了,对于有基础的朋友,就可以根据自己需求修改模板。 3、网站首页的封面如何设置 设置封面非常简单,只需要把视频推荐设置为推荐 9 即可:

点击视频编辑,上传视频的海报图,即可:

刷新官网,可以看到封面已设置:

当设置多个封面后,轮播图功能并为生效:

查看模板源码,发现轮播图功能源码在 script.js 文件实现:

打开浏览器控制台,发现并未加载轮播图的 js 文件: 当知道问题出在什么地方,解决就非常简单了,通过查看源码,发现 js 文件都在这里设置的,那么只需要把 script.js 路径加上即可:

当修改好代码后,刷新官网,此时轮播图功能就正常了。 4、模板不好看如何安装其它模板 在网上搜索 maccms10 模板,这个就不多介绍了:


当找到心仪的模板后,下载源码:

一般都会有模板安装的教程:

仿爱电影 MizhiADY 模板源码下载:仿爱电影 MizhiADY 板源码.zip

上传模板源码到网站 template 目录,解压后把 mizhiady 文件移动到 template 下即可:

在管理后台模板中,可以看见模板已上传成功:

设置网站的模板:

设置成功后,按照教程先刷新官网:

设置主题后台地址: MZADY 觅知主题,/mac.php/admin/mizhiady/mzadyset

保存后,刷新管理后台,发现左侧菜单新增 MZADY 主题,进入主题,可以进行相关设置:

至此,安装其它模板的流程就结束了,关于模板的选择全凭个人爱好了,值得注意的是,网上的模板可能存在一些广告。 5、如何通过域名访问网站 网站通过域名访问的前提是得有一个域名,如何购买域名这里就略过了。 在宝塔面板,网站列表中,点击设置:

这里填写需要访问的域名地址:

在域名服务商进行域名解析,这里以 CF 为例:

通过域名访问:

此处,域名访问成功设置。 此时,浏览器提示不安全,是因为网站未设置 SSL 证书,接下来,继续为网站设置证书。为什么给网站需要设置证书?这里简单说明下,设置证书后网站会更安全。 如何给网站设置证书呢?需要先申请 SSL 证书,这里以 VMRack 的证书为例,主要是永久免费还能自动续费。 登录 VMRack 控制台,进入 SSL 证书页面,在右上角点击申请证书:

接下来需要在域名服务商填写 CNAME 域名解析:

这里还是以 CF 为例:

填写完成后,回到控制台,点击验证解析记录:

当状态都成功后,点击申请证书:

只需要等待几分钟即可:

点击管理,即可查看证书的详情信息:

下载证书:

只需要把证书,上传到宝塔的 SSL:

把前面申请好的证书信息,分别复制 / 粘贴过来即可:

可以看到证书已上传成功:

在网站设置中,直接部署证书:

开启强制 HTTPS 访问:

访问官网,此时浏览器已经未提示不安全了:

至此,苹果 CMS 的搭建;一些简单使用;通过域名访问以及简单的源码修改等,都已简单介绍,如果对此感兴趣的朋友欢迎来,站长破壁者交流群共同探讨学习,点击进入交流群。


📌 转载信息
原作者:
Rosna
转载时间:
2026/1/15 18:20:51

地址 https://kiro.endpoint.cc.cd

使用 Cloudflare Worker 绕过 CORS 限制,实现生成 Device Flow 登录链接
没有做信息存储!!
源代码:

/**
 * worker.js — Kiro Manual Auth (Device Flow) Web Tool
 * Full UI (no cuts): Vercel/shadcn/Inspira-ish + Acrylic hover actions
 *
 * ✅ Flow:
 * Login -> get CID/CS -> get Device Flow link (hover: Copy | Open Link) -> background polling
 * -> success modal pops with Tabs: Viewer / JSON
 *
 * ✅ Viewer tab:
 * - Shows CID / CS + ALL keys from last successful /api/poll JSON
 * - Each row hover shows acrylic blur + "Copy" (same logic as URL hover, no OpenLink)
 *
 * ✅ Poll logic:
 * - No overlapping polls: next poll only after previous finished
 * - If backend returns 400 {"error":"authorization_pending"...}, treat as Pending (yellow) not error
 *
 * ✅ NEW (your request):
 * - Modal content is scrollable (so long tokens won't squeeze/overflow the screen)
 * - Each KV value area is also scrollable (does not cut value; copy still copies full value)
 */

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    const path = url.pathname;

    // ===== Config =====
    const OIDC = "https://oidc.us-east-1.amazonaws.com";
    const PORTAL = "https://view.awsapps.com";
    const START_URL = `${PORTAL}/start`;

    // ===== CORS =====
    const corsHeaders = {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "GET,POST,OPTIONS",
      "Access-Control-Allow-Headers": "Content-Type",
    };

    if (request.method === "OPTIONS") {
      return new Response(null, { status: 204, headers: corsHeaders });
    }

    // ===== UI =====
    if (request.method === "GET" && path === "/") {
      return new Response(renderHTML(), {
        headers: {
          "content-type": "text/html; charset=utf-8",
          "cache-control": "no-store",
        },
      });
    }

    // ===== Helpers =====
    const json = (obj, status = 200, extraHeaders = {}) =>
      new Response(JSON.stringify(obj), {
        status,
        headers: {
          "content-type": "application/json; charset=utf-8",
          "cache-control": "no-store",
          ...corsHeaders,
          ...extraHeaders,
        },
      });

    const proxyJson = async (targetUrl, bodyObj) => {
      const r = await fetch(targetUrl, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify(bodyObj),
      });
      const text = await r.text();
      return new Response(text, {
        status: r.status,
        headers: {
          "content-type": "application/json; charset=utf-8",
          "cache-control": "no-store",
          ...corsHeaders,
        },
      });
    };

    // ===== API: register =====
    if (request.method === "POST" && path === "/api/register") {
      return proxyJson(`${OIDC}/client/register`, {
        clientName: "Amazon Q Developer for command line",
        clientType: "public",
        scopes: [
          "codewhisperer:completions",
          "codewhisperer:analysis",
          "codewhisperer:conversations",
        ],
      });
    }

    // ===== API: device authorization =====
    if (request.method === "POST" && path === "/api/device") {
      let req;
      try {
        req = await request.json();
      } catch {
        req = {};
      }
      const { clientId, clientSecret } = req || {};
      if (!clientId || !clientSecret) {
        return json({ error: "missing clientId/clientSecret" }, 400);
      }
      return proxyJson(`${OIDC}/device_authorization`, {
        clientId,
        clientSecret,
        startUrl: START_URL,
      });
    }

    // ===== API: poll token (device_code) =====
    // UI handles 400 authorization_pending as "Pending"
    if (request.method === "POST" && path === "/api/poll") {
      let req;
      try {
        req = await request.json();
      } catch {
        req = {};
      }
      const { clientId, clientSecret, deviceCode } = req || {};
      if (!clientId || !clientSecret || !deviceCode) {
        return json({ error: "missing clientId/clientSecret/deviceCode" }, 400);
      }
      return proxyJson(`${OIDC}/token`, {
        clientId,
        clientSecret,
        deviceCode,
        grantType: "urn:ietf:params:oauth:grant-type:device_code",
      });
    }

    // ===== API: refresh (optional) =====
    if (request.method === "POST" && path === "/api/refresh") {
      let req;
      try {
        req = await request.json();
      } catch {
        req = {};
      }
      const { clientId, clientSecret, refreshToken } = req || {};
      if (!clientId || !clientSecret || !refreshToken) {
        return json({ error: "missing clientId/clientSecret/refreshToken" }, 400);
      }
      return proxyJson(`${OIDC}/token`, {
        clientId,
        clientSecret,
        refreshToken,
        grantType: "refresh_token",
      });
    }

    return new Response("Not Found", { status: 404 });
  },
};

function renderHTML() {
  const AWS_SVG = `<svg fill="currentColor" fill-rule="evenodd" height="18" viewBox="0 0 24 24" width="18" xmlns="http://www.w3.org/2000/svg" style="flex: 0 0 auto; line-height: 1;"><title>AWS</title><path d="M6.763 11.212c0 .296.032.535.088.71.064.176.144.368.256.576.04.063.056.127.056.183 0 .08-.048.16-.152.24l-.503.335a.383.383 0 01-.208.072c-.08 0-.16-.04-.239-.112a2.47 2.47 0 01-.287-.375 6.18 6.18 0 01-.248-.471c-.622.734-1.405 1.101-2.347 1.101-.67 0-1.205-.191-1.596-.574-.39-.384-.59-.894-.59-1.533 0-.678.24-1.23.726-1.644.487-.415 1.133-.623 1.955-.623.272 0 .551.024.846.064.296.04.6.104.918.176v-.583c0-.607-.127-1.03-.375-1.277-.255-.248-.686-.367-1.3-.367-.28 0-.568.031-.863.103-.295.072-.583.16-.862.272a2.4 2.4 0 01-.28.104.488.488 0 01-.127.023c-.112 0-.168-.08-.168-.247v-.391c0-.128.016-.224.056-.28a.597.597 0 01.224-.167 4.577 4.577 0 011.005-.36 4.84 4.84 0 011.246-.151c.95 0 1.644.216 2.091.647.44.43.662 1.085.662 1.963v2.586h.016zm-3.24 1.214c.263 0 .534-.048.822-.144a1.78 1.78 0 00.758-.51 1.27 1.27 0 00.272-.512c.047-.191.08-.423.08-.694v-.335a6.66 6.66 0 00-.735-.136 6.02 6.02 0 00-.75-.048c-.535 0-.926.104-1.19.32-.263.215-.39.518-.39.917 0 .375.095.655.295.846.191.2.47.296.838.296zm6.41.862c-.144 0-.24-.024-.304-.08-.064-.048-.12-.16-.168-.311L7.586 6.726a1.398 1.398 0 01-.072-.32c0-.128.064-.2.191-.2h.783c.151 0 .255.025.31.08.065.048.113.16.16.312l1.342 5.284 1.245-5.284c.04-.16.088-.264.151-.312a.549.549 0 01.32-.08h.638c.152 0 .256.025.32.08.063.048.12.16.151.312l1.261 5.348 1.381-5.348c.048-.16.104-.264.16-.312a.52.52 0 01.311-.08h.743c.127 0 .2.065.2.2 0 .04-.009.08-.017.128a1.137 1.137 0 01-.056.2l-1.923 6.17c-.048.16-.104.263-.168.311a.51.51 0 01-.303.08h-.687c-.15 0-.255-.024-.32-.08-.063-.056-.119-.16-.15-.32L12.32 7.747l-1.23 5.14c-.04.16-.087.264-.15.32-.065.056-.177.08-.32.08l-.686.001zm10.256.215c-.415 0-.83-.048-1.229-.143-.399-.096-.71-.2-.918-.32-.128-.071-.215-.151-.247-.223a.563.563 0 01-.048-.224v-.407c0-.167.064-.247.183-.247.048 0 .096.008.144.024.048.016.12.048.2.08.271.12.566.215.878.279.32.064.63.096.95.096.502 0 .894-.088 1.165-.264a.86.86 0 00.415-.758.777.777 0 00-.215-.559c-.144-.151-.416-.287-.807-.415l-1.157-.36c-.583-.183-1.014-.454-1.277-.813a1.902 1.902 0 01-.4-1.158c0-.335.073-.63.216-.886.144-.255.335-.479.575-.654.24-.184.51-.32.83-.415.32-.096.655-.136 1.006-.136.175 0 .36.008.535.032.183.024.35.056.518.088.16.04.312.08.455.127.144.048.256.096.336.144a.69.69 0 01.24.2.43.43 0 01.071.263v.375c0 .168-.064.256-.184.256a.83.83 0 01-.303-.096 3.652 3.652 0 00-1.532-.311c-.455 0-.815.071-1.062.223-.248.152-.375.383-.375.71 0 .224.08.416.24.567.16.152.454.304.877.44l1.134.358c.574.184.99.44 1.237.767.247.327.367.702.367 1.117 0 .343-.072.655-.207.926a2.157 2.157 0 01-.583.703c-.248.2-.543.343-.886.447-.36.111-.734.167-1.142.167z"></path><path d="M.378 15.475c3.384 1.963 7.56 3.153 11.877 3.153 2.914 0 6.114-.607 9.06-1.852.44-.2.814.287.383.607-2.626 1.94-6.442 2.969-9.722 2.969-4.598 0-8.74-1.7-11.87-4.526-.247-.223-.024-.527.272-.351zm23.531-.2c.287.36-.08 2.826-1.485 4.007-.215.184-.423.088-.327-.151l.175-.439c.343-.88.802-2.198.52-2.555-.336-.43-2.22-.207-3.074-.103-.255.032-.295-.192-.063-.36 1.5-1.053 3.967-.75 4.254-.399z" fill="#F90"></path></svg>`;

  return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Kiro Manual Auth (Worker)</title>
<style>
  :root{
    --bg: 10 10 12;
    --card: 18 18 22;
    --muted: 160 160 175;
    --text: 240 240 245;
    --border: 255 255 255;
    --shadow: 0 10px 30px rgba(0,0,0,.35);
    --ring: 99 102 241;
    --ok: 16 185 129;
    --bad: 239 68 68;
    --warn: 245 158 11;
  }
  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0;
    font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
    color: rgb(var(--text));
    background:
      radial-gradient(1200px 600px at 20% 10%, rgba(99,102,241,.22), transparent 60%),
      radial-gradient(900px 520px at 80% 30%, rgba(16,185,129,.18), transparent 55%),
      radial-gradient(900px 520px at 40% 90%, rgba(236,72,153,.14), transparent 55%),
      linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,.35) 55%, rgba(0,0,0,.75) 100%),
      rgb(var(--bg));
    overflow-x:hidden;
  }

  .container{max-width:980px;margin:0 auto;padding:40px 16px 60px}
  .header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:16px}
  .title{font-size:22px;font-weight:650;letter-spacing:-.02em;margin:0}
  .subtitle{margin:8px 0 0;color:rgba(var(--muted),.9);font-size:13px;line-height:1.5}

  .card{
    border:1px solid rgba(var(--border),.10);
    background: rgba(var(--card), .55);
    backdrop-filter: blur(16px);
    -webkit-backdrop-filter: blur(16px);
    border-radius: 18px;
    box-shadow: var(--shadow);
    overflow:hidden;
    position:relative;
  }
  .card-inner{padding:18px}
  .row{display:flex;gap:12px;flex-wrap:wrap;align-items:center;justify-content:space-between}
  .muted{color:rgba(var(--muted),.9);font-size:12px;line-height:1.5}
  .sep{height:1px;background:rgba(var(--border),.10);margin:16px 0}

  .btn{
    display:inline-flex;align-items:center;gap:10px;
    padding:10px 14px;
    border-radius:14px;
    border:1px solid rgba(var(--border),.14);
    background: rgba(255,255,255,.06);
    color: rgb(var(--text));
    cursor:pointer;
    font-size:13px;font-weight:600;
    transition: transform .12s ease, background .12s ease, border-color .12s ease, box-shadow .12s ease;
    user-select:none;
  }
  .btn:hover{background: rgba(255,255,255,.09);border-color: rgba(var(--border),.20);transform: translateY(-1px)}
  .btn:active{transform: translateY(0px)}
  .btn:focus{outline:none;box-shadow: 0 0 0 4px rgba(var(--ring), .25)}
  .btn[disabled]{opacity:.55;cursor:not-allowed;transform:none}
  .btn-primary{background: rgba(255,255,255,.10);border-color: rgba(255,255,255,.16)}
  .btn-ghost{background: transparent;border-color: rgba(255,255,255,.10)}
  .btn-secondary{background: rgba(255,255,255,.07)}
  .btn-xs{padding:7px 10px;border-radius:12px;font-size:12px}

  .grid{display:grid;grid-template-columns:1fr;gap:12px}
  @media (min-width: 860px){ .grid{grid-template-columns:1fr 1fr} }

  .field{
    border:1px solid rgba(var(--border),.10);
    background: rgba(0,0,0,.22);
    border-radius: 16px;
    padding:12px 14px;
  }
  .label{font-size:11px;color:rgba(var(--muted),.9);margin-bottom:6px}
  .value{font-size:13px;word-break:break-all}
  .mono{font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size:12px}

  .link-panel{
    position:relative;
    border:1px solid rgba(var(--border),.10);
    background: rgba(0,0,0,.20);
    border-radius: 18px;
    padding:14px;
    overflow:hidden;
    transition: box-shadow .16s ease, border-color .16s ease, transform .16s ease;
  }
  .link-panel:hover{box-shadow: 0 12px 30px rgba(0,0,0,.35);border-color: rgba(255,255,255,.16);transform: translateY(-1px)}
  .link-text{margin-top:8px;font-size:13px;opacity:.92;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

  /* Acrylic hover overlay */
  .acrylic{position:absolute;inset:0;opacity:0;transition: opacity .16s ease;pointer-events:none;}
  .link-panel:hover .acrylic{opacity:1}
  .acrylic::before{
    content:"";position:absolute;inset:0;
    background: rgba(255,255,255,.10);
    backdrop-filter: blur(18px);
    -webkit-backdrop-filter: blur(18px);
  }
  .acrylic::after{
    content:"";position:absolute;inset:-40px;
    background: radial-gradient(420px 220px at 20% 20%, rgba(255,255,255,.12), transparent 55%),
                radial-gradient(420px 220px at 80% 40%, rgba(255,255,255,.08), transparent 55%);
    opacity:.9;mix-blend-mode: overlay;
  }
  .link-actions{
    position:absolute; inset:0;
    display:flex;align-items:center;justify-content:center;gap:10px;
    opacity:0;transition: opacity .16s ease;
    pointer-events:none;
  }
  .link-panel:hover .link-actions{opacity:1}
  .link-actions .btn{pointer-events:auto}

  .status{
    border:1px solid rgba(var(--border),.10);
    background: rgba(0,0,0,.20);
    border-radius: 16px;
    padding:12px 14px;
    display:flex;gap:12px;align-items:flex-start;justify-content:space-between;
  }
  .badge{
    display:inline-flex;align-items:center;gap:8px;
    font-size:11px;color:rgba(var(--muted),.95);
    padding:6px 10px;border-radius:999px;
    border:1px solid rgba(var(--border),.12);
    background: rgba(255,255,255,.05);
    white-space:nowrap;
  }
  .dot{width:8px;height:8px;border-radius:999px;background: rgba(255,255,255,.55);position:relative;}
  .dot.ping::after{
    content:"";position:absolute;inset:-6px;border-radius:999px;
    border:1px solid rgba(255,255,255,.35);
    animation: ping 1.2s ease-out infinite;opacity:.8;
  }
  @keyframes ping{0%{transform:scale(.4);opacity:.8} 100%{transform:scale(1.5);opacity:0}}

  .status.ok{border-color: rgba(var(--ok), .35); background: rgba(var(--ok), .10)}
  .status.bad{border-color: rgba(var(--bad), .35); background: rgba(var(--bad), .10)}
  .status.warn{border-color: rgba(var(--warn), .35); background: rgba(var(--warn), .08)}

  /* Modal */
  .modal-backdrop{
    position:fixed;inset:0;
    background: rgba(0,0,0,.60);
    display:none;
    align-items:center;justify-content:center;
    padding:18px;
    z-index:50;
  }
  .modal-backdrop.show{display:flex}
  .modal{
    width:min(900px, 100%);
    max-height: 88vh; /* NEW: keep modal within viewport */
    border-radius: 18px;
    border:1px solid rgba(var(--border),.14);
    background: rgba(var(--card), .86);
    backdrop-filter: blur(18px);
    -webkit-backdrop-filter: blur(18px);
    box-shadow: var(--shadow);
    overflow:hidden;
    display:flex;
    flex-direction:column; /* NEW: allow internal scroll areas */
  }
  .modal-head{padding:16px 18px;border-bottom:1px solid rgba(var(--border),.10);flex:0 0 auto}
  .modal-title{margin:0;font-size:16px;font-weight:750}
  .modal-desc{margin:6px 0 0;color:rgba(var(--muted),.9);font-size:12px;line-height:1.45}

  .modal-body{
    padding:16px 18px;
    flex: 1 1 auto;          /* NEW */
    min-height: 0;           /* NEW: critical for flex scroll children */
    display:flex;            /* NEW */
    flex-direction:column;   /* NEW */
    gap:12px;                /* NEW */
  }

  /* NEW: scrollable content area inside modal body */
  .modal-scroll{
    flex: 1 1 auto;
    min-height: 0;
    overflow:auto;
    padding-right: 4px;
    border-radius: 14px;
  }
  /* nicer scrollbar (webkit only) */
  .modal-scroll::-webkit-scrollbar{width:10px}
  .modal-scroll::-webkit-scrollbar-thumb{background: rgba(255,255,255,.12); border-radius: 999px; border:2px solid rgba(0,0,0,.15)}
  .modal-scroll::-webkit-scrollbar-track{background: rgba(0,0,0,.10); border-radius: 999px}

  pre{
    margin:0;border-radius: 14px;border:1px solid rgba(var(--border),.10);
    background: rgba(0,0,0,.30);
    padding:14px;
    overflow:auto;
    font-size:12px; line-height:1.5;
  }

  /* Footer pinned inside modal */
  .footer-actions{
    display:flex;gap:10px;flex-wrap:wrap;
    padding-top: 12px;
    margin-top: 0;
    position: sticky;     /* NEW */
    bottom: 0;            /* NEW */
    background: linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,.20) 30%, rgba(0,0,0,.28));
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    border-top: 1px solid rgba(255,255,255,.08);
  }
  .right{margin-left:auto}

  /* Tabs */
  .tabs{display:flex;gap:8px;align-items:center}
  .tab{
    padding:8px 10px;
    border-radius: 12px;
    border:1px solid rgba(var(--border),.12);
    background: rgba(255,255,255,.05);
    color: rgba(var(--text), .88);
    font-size:12px;font-weight:650;
    cursor:pointer;
    transition: background .12s ease, border-color .12s ease, transform .12s ease;
    user-select:none;
  }
  .tab:hover{background: rgba(255,255,255,.08);border-color: rgba(255,255,255,.18);transform: translateY(-1px)}
  .tab.active{
    background: rgba(255,255,255,.12);
    border-color: rgba(255,255,255,.22);
    color: rgb(var(--text));
  }

  /* Viewer list */
  .viewer{display:flex;flex-direction:column;gap:10px;}
  .kv-row{
    position:relative;
    border:1px solid rgba(var(--border),.10);
    background: rgba(0,0,0,.22);
    border-radius: 16px;
    padding:12px 14px;
    overflow:hidden;
  }
  .kv-row:hover{border-color: rgba(255,255,255,.16)}
  .kv-key{font-size:11px;color:rgba(var(--muted),.92)}
  .kv-val{
    margin-top:6px;
    font-size:12px;
    word-break: break-all;
    opacity:.92;

    /* NEW: prevent a single token from taking the entire screen */
    max-height: 140px;
    overflow:auto;
    padding-right: 6px;
  }
  .kv-val::-webkit-scrollbar{width:10px}
  .kv-val::-webkit-scrollbar-thumb{background: rgba(255,255,255,.10); border-radius: 999px; border:2px solid rgba(0,0,0,.15)}
  .kv-val::-webkit-scrollbar-track{background: rgba(0,0,0,.10); border-radius: 999px}

  .kv-actions{
    position:absolute;inset:0;
    display:flex;align-items:center;justify-content:center;
    opacity:0;transition: opacity .16s ease;
    pointer-events:none;
  }
  .kv-row:hover .kv-actions{opacity:1}
  .kv-actions .btn{pointer-events:auto}

  .kv-acrylic{position:absolute;inset:0;opacity:0;transition: opacity .16s ease;pointer-events:none;}
  .kv-row:hover .kv-acrylic{opacity:1}
  .kv-acrylic::before{
    content:"";position:absolute;inset:0;
    background: rgba(255,255,255,.10);
    backdrop-filter: blur(18px);
    -webkit-backdrop-filter: blur(18px);
  }
  .kv-acrylic::after{
    content:"";position:absolute;inset:-40px;
    background: radial-gradient(380px 200px at 25% 30%, rgba(255,255,255,.12), transparent 58%),
                radial-gradient(380px 200px at 75% 45%, rgba(255,255,255,.08), transparent 60%);
    opacity:.9;mix-blend-mode: overlay;
  }

  .small-note{margin-top:12px;color:rgba(var(--muted),.85);font-size:12px;line-height:1.5}
  .glow{
    position:absolute; inset:-200px;
    background: radial-gradient(600px 260px at 20% 10%, rgba(99,102,241,.20), transparent 55%),
                radial-gradient(500px 240px at 80% 20%, rgba(16,185,129,.14), transparent 60%);
    pointer-events:none;
    opacity:.9;
  }
</style>
</head>

<body>
  <div class="container">
    <div class="header">
      <div>
        <h1 class="title">Kiro Manual Auth</h1>
        <p class="subtitle">
          Click <b>Login</b> → get CID/CS → device flow link. Hover link for <b>Copy | Open Link</b>.
          We poll automatically and pop credentials when authorized.
        </p>
      </div>
      <button id="btnLogin" class="btn btn-primary">
        ${AWS_SVG}
        <span>Login</span>
      </button>
    </div>

    <div class="card">
      <div class="glow"></div>
      <div class="card-inner">
        <div class="row" style="gap:10px">
          <div class="muted">
            OIDC requests are proxied server-side to bypass browser CORS.
            <span style="display:block;opacity:.85">Treat Refresh Token / Client Secret as secrets.</span>
          </div>
          <div class="row" style="justify-content:flex-end">
            <button id="btnClear" class="btn btn-ghost">Clear</button>
          </div>
        </div>

        <div class="sep"></div>

        <div class="link-panel" id="linkPanel">
          <div class="label">Device Flow Link</div>
          <div class="link-text" id="verifyUrl" title="">—</div>

          <div class="acrylic"></div>
          <div class="link-actions" id="linkActions">
            <button id="btnCopyLink" class="btn btn-secondary">Copy</button>
            <button id="btnOpenLink" class="btn btn-secondary">Open Link</button>
          </div>
        </div>

        <div class="sep"></div>

        <div class="grid">
          <div class="field">
            <div class="label">User Code</div>
            <div class="value" id="userCode">—</div>
          </div>
          <div class="field">
            <div class="label">Device Code</div>
            <div class="value mono" id="deviceCode">—</div>
          </div>
        </div>

        <div class="sep"></div>

        <div class="status" id="statusBox">
          <div>
            <div style="font-weight:700;font-size:13px">Status</div>
            <div class="muted" id="statusText" style="margin-top:4px">Click Login to start.</div>
          </div>
          <div class="badge" id="statusBadge">
            <span class="dot" id="statusDot"></span>
            <span id="statusLabel">IDLE</span>
          </div>
        </div>

        <div class="small-note">
          Tip: hover the link panel to copy/open. Polling starts immediately after link generation.
        </div>
      </div>
    </div>
  </div>

  <!-- Modal -->
  <div class="modal-backdrop" id="modalBackdrop" role="dialog" aria-modal="true">
    <div class="modal">
      <div class="modal-head">
        <div class="row" style="align-items:flex-start">
          <div>
            <h2 class="modal-title">Authorized ✅</h2>
            <p class="modal-desc">Viewer: copy per key (CID/CS included). JSON: raw token.json.</p>
          </div>
          <div class="tabs" aria-label="result tabs">
            <div id="tabViewer" class="tab active">Viewer</div>
            <div id="tabJson" class="tab">JSON</div>
          </div>
        </div>
      </div>

      <div class="modal-body">
        <!-- NEW: scroll container -->
        <div class="modal-scroll" id="modalScroll">
          <div id="panelViewer" class="viewer"></div>

          <div id="panelJson" style="display:none">
            <pre id="resultPre">{}</pre>
          </div>
        </div>

        <div class="footer-actions">
          <button id="btnCopyJson" class="btn btn-secondary">Copy JSON</button>
          <button id="btnDownload" class="btn btn-secondary">Download token.json</button>
          <button id="btnCloseModal" class="btn btn-ghost right">Close</button>
        </div>
      </div>
    </div>
  </div>

<script>
(() => {
  const $ = (id) => document.getElementById(id);

  const state = {
    loading: false,
    clientId: "",
    clientSecret: "",
    verifyUrl: "",
    userCode: "",
    deviceCode: "",

    // polling control: no overlap; next only after last finished
    pollTimer: null,
    pollIntervalMs: 2000,
    pollInFlight: false,
    pollingEnabled: false,

    lastPollOk: null,   // last successful poll JSON (accessToken present)
    resultJson: "",     // token.json content shown in JSON tab
  };

  const ui = {
    btnLogin: $("btnLogin"),
    btnClear: $("btnClear"),
    verifyUrl: $("verifyUrl"),
    userCode: $("userCode"),
    deviceCode: $("deviceCode"),
    btnCopyLink: $("btnCopyLink"),
    btnOpenLink: $("btnOpenLink"),

    statusBox: $("statusBox"),
    statusText: $("statusText"),
    statusDot: $("statusDot"),
    statusLabel: $("statusLabel"),

    modalBackdrop: $("modalBackdrop"),
    modalScroll: $("modalScroll"),
    panelViewer: $("panelViewer"),
    panelJson: $("panelJson"),
    resultPre: $("resultPre"),
    btnCopyJson: $("btnCopyJson"),
    btnDownload: $("btnDownload"),
    btnCloseModal: $("btnCloseModal"),

    tabViewer: $("tabViewer"),
    tabJson: $("tabJson"),
  };

  function escapeHtml(str) {
    return String(str)
      .replaceAll("&", "&amp;")
      .replaceAll("<", "&lt;")
      .replaceAll(">", "&gt;")
      .replaceAll('"', "&quot;")
      .replaceAll("'", "&#039;");
  }

  function setBusy(b) {
    state.loading = b;
    ui.btnLogin.disabled = b;
    ui.btnClear.disabled = b;
  }

  function setStatus(kind, text, label) {
    ui.statusText.textContent = text;
    ui.statusLabel.textContent = label;

    ui.statusBox.classList.remove("ok", "bad", "warn");
    ui.statusDot.classList.remove("ping");
    ui.statusDot.style.background = "rgba(255,255,255,.55)";

    if (kind === "pending" || kind === "polling") {
      ui.statusBox.classList.add("warn");
      ui.statusDot.classList.add("ping");
      ui.statusDot.style.background = "rgba(245,158,11,.9)";
    } else if (kind === "ok") {
      ui.statusBox.classList.add("ok");
      ui.statusDot.style.background = "rgba(16,185,129,.95)";
    } else if (kind === "error") {
      ui.statusBox.classList.add("bad");
      ui.statusDot.style.background = "rgba(239,68,68,.95)";
    }
  }

  function stopPolling() {
    state.pollingEnabled = false;
    if (state.pollTimer) clearTimeout(state.pollTimer);
    state.pollTimer = null;
    state.pollInFlight = false;
  }

  function scheduleNextPoll() {
    if (!state.pollingEnabled) return;
    if (state.pollTimer) clearTimeout(state.pollTimer);

    state.pollTimer = setTimeout(async () => {
      await pollOnce();
      scheduleNextPoll();
    }, state.pollIntervalMs);
  }

  async function apiStrict(path, body) {
    const r = await fetch(path, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: body ? JSON.stringify(body) : undefined
    });
    const data = await r.json().catch(() => ({}));
    if (!r.ok) throw new Error(typeof data === "string" ? data : JSON.stringify(data));
    return data;
  }

  async function apiAllowNon200(path, body) {
    const r = await fetch(path, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: body ? JSON.stringify(body) : undefined
    });
    const data = await r.json().catch(() => ({}));
    return { ok: r.ok, status: r.status, data };
  }

  function setLink(url) {
    state.verifyUrl = url || "";
    ui.verifyUrl.textContent = url || "—";
    ui.verifyUrl.title = url || "";
    ui.btnCopyLink.disabled = !url;
    ui.btnOpenLink.disabled = !url;
  }

  function setCodes({ userCode, deviceCode }) {
    state.userCode = userCode || "";
    state.deviceCode = deviceCode || "";
    ui.userCode.textContent = userCode || "—";
    ui.deviceCode.textContent = deviceCode || "—";
  }

  async function copy(text) {
    if (!text) return;
    await navigator.clipboard.writeText(text);
  }

  function showModal() {
    ui.modalBackdrop.classList.add("show");
    // NEW: reset scroll to top when opening (optional but nice)
    if (ui.modalScroll) ui.modalScroll.scrollTop = 0;
  }
  function hideModal() {
    ui.modalBackdrop.classList.remove("show");
  }

  function setTab(which) {
    const viewer = which === "viewer";
    ui.tabViewer.classList.toggle("active", viewer);
    ui.tabJson.classList.toggle("active", !viewer);
    ui.panelViewer.style.display = viewer ? "flex" : "none";
    ui.panelJson.style.display = viewer ? "none" : "block";
    // keep scroll at top when switching
    if (ui.modalScroll) ui.modalScroll.scrollTop = 0;
  }

  function kvRow(key, val, onCopy) {
    const row = document.createElement("div");
    row.className = "kv-row";
    row.innerHTML = \`
      <div class="kv-key">\${escapeHtml(key)}</div>
      <div class="kv-val mono">\${escapeHtml(val)}</div>
      <div class="kv-acrylic"></div>
      <div class="kv-actions">
        <button class="btn btn-secondary btn-xs">Copy</button>
      </div>
    \`;
    const btn = row.querySelector("button");
    btn.addEventListener("click", onCopy);
    return row;
  }

  function normalizeValue(v) {
    if (v === null || v === undefined) return "null";
    if (typeof v === "string") return v;
    return JSON.stringify(v);
  }

  function renderViewer({ cid, cs, pollOk }) {
    ui.panelViewer.innerHTML = "";

    const cidVal = cid || "";
    const csVal = cs || "";

    ui.panelViewer.appendChild(
      kvRow("clientId", cidVal || "—", async () => {
        await copy(cidVal);
        setStatus("ok", "Copied: clientId", "SUCCESS");
      })
    );

    ui.panelViewer.appendChild(
      kvRow("clientSecret", csVal || "—", async () => {
        await copy(csVal);
        setStatus("ok", "Copied: clientSecret", "SUCCESS");
      })
    );

    if (!pollOk || typeof pollOk !== "object") {
      const msg = document.createElement("div");
      msg.className = "muted";
      msg.textContent = "No poll response data.";
      ui.panelViewer.appendChild(msg);
      return;
    }

    const entries = Object.entries(pollOk);
    for (const [k, v] of entries) {
      const val = normalizeValue(v);
      ui.panelViewer.appendChild(
        kvRow(k, val, async () => {
          await copy(val);
          setStatus("ok", "Copied: " + k, "SUCCESS");
        })
      );
    }
  }

  function buildTokenJson(cid, cs, pollOk) {
    const out = {
      client_id: cid,
      client_secret: cs,
      refresh_token: pollOk && pollOk.refreshToken ? pollOk.refreshToken : null,
      poll_response: pollOk || null
    };
    return JSON.stringify(out, null, 2);
  }

  async function pollOnce() {
    if (!state.pollingEnabled) return;
    if (state.pollInFlight) return;

    state.pollInFlight = true;

    try {
      const { ok, status, data } = await apiAllowNon200("/api/poll", {
        clientId: state.clientId,
        clientSecret: state.clientSecret,
        deviceCode: state.deviceCode,
      });

      if (data && data.accessToken) {
        state.lastPollOk = data;
        stopPolling();
        setStatus("ok", "Authorized! Refresh token received.", "SUCCESS");

        state.resultJson = buildTokenJson(state.clientId, state.clientSecret, data);
        ui.resultPre.textContent = state.resultJson;

        renderViewer({ cid: state.clientId, cs: state.clientSecret, pollOk: data });

        setTab("viewer");
        showModal();
        return;
      }

      // PENDING (even if backend returns 400)
      if (data && data.error === "authorization_pending") {
        setStatus("pending", "Pending authorization... (complete it in the opened page)", "PENDING");
        return;
      }

      if (data && data.error === "slow_down") {
        state.pollIntervalMs = Math.min(state.pollIntervalMs + 2000, 10000);
        setStatus(
          "pending",
          "Slow down requested. Polling every " + (state.pollIntervalMs / 1000).toFixed(1) + "s",
          "PENDING"
        );
        return;
      }

      if (data && data.error === "expired_token") {
        stopPolling();
        setStatus("error", "Device code expired. Click Login again.", "EXPIRED");
        return;
      }

      if (!ok) {
        stopPolling();
        setStatus("error", "Error: " + JSON.stringify(data), "ERROR");
        return;
      }

      setStatus("pending", "Waiting... " + JSON.stringify(data), "PENDING");
    } catch (e) {
      stopPolling();
      setStatus("error", "Polling error: " + (e && e.message ? e.message : String(e)), "ERROR");
    } finally {
      state.pollInFlight = false;
    }
  }

  function startPolling() {
    stopPolling();
    state.pollingEnabled = true;
    setStatus("pending", "Polling started... authorize in the verification page.", "PENDING");

    pollOnce().finally(() => {
      scheduleNextPoll();
    });
  }

  async function loginFlow() {
    stopPolling();
    setBusy(true);

    setLink("");
    setCodes({ userCode: "", deviceCode: "" });
    state.clientId = "";
    state.clientSecret = "";
    state.lastPollOk = null;
    state.resultJson = "";

    setStatus("polling", "Registering OIDC client...", "WORKING");

    try {
      const reg = await apiStrict("/api/register");
      state.clientId = reg.clientId;
      state.clientSecret = reg.clientSecret;

      setStatus("polling", "Generating device flow link...", "WORKING");

      const dev = await apiStrict("/api/device", {
        clientId: state.clientId,
        clientSecret: state.clientSecret
      });

      const link = dev.verificationUriComplete || dev.verificationUri || "";
      setLink(link);
      setCodes({ userCode: dev.userCode, deviceCode: dev.deviceCode });

      state.pollIntervalMs = Math.max(2000, (dev.interval ? dev.interval * 1000 : 2000));
      setStatus("pending", "Link ready. Hover to Copy/Open. Polling in background...", "PENDING");

      startPolling();
    } catch (e) {
      stopPolling();
      setStatus("error", "Error: " + (e && e.message ? e.message : String(e)), "ERROR");
    } finally {
      setBusy(false);
    }
  }

  function clearAll() {
    stopPolling();
    state.clientId = "";
    state.clientSecret = "";
    state.verifyUrl = "";
    state.userCode = "";
    state.deviceCode = "";
    state.lastPollOk = null;
    state.resultJson = "";
    state.pollIntervalMs = 2000;

    setLink("");
    setCodes({ userCode: "", deviceCode: "" });
    setStatus("idle", "Cleared. Click Login to start.", "IDLE");
    hideModal();
  }

  // ===== Bindings =====
  ui.btnLogin.addEventListener("click", loginFlow);
  ui.btnClear.addEventListener("click", clearAll);

  ui.btnCopyLink.addEventListener("click", async () => {
    if (!state.verifyUrl) return;
    await copy(state.verifyUrl);
    setStatus("pending", "Link copied. Continue authorization in the opened page.", "PENDING");
  });

  ui.btnOpenLink.addEventListener("click", () => {
    if (!state.verifyUrl) return;
    window.open(state.verifyUrl, "_blank", "noopener,noreferrer");
    setStatus("pending", "Link opened. Complete authorization, we are polling...", "PENDING");
  });

  ui.btnCopyJson.addEventListener("click", async () => {
    if (!state.resultJson) return;
    await copy(state.resultJson);
    setStatus("ok", "token.json copied.", "SUCCESS");
  });

  ui.btnDownload.addEventListener("click", () => {
    if (!state.resultJson) return;
    const blob = new Blob([state.resultJson], { type: "application/json" });
    const a = document.createElement("a");
    a.href = URL.createObjectURL(blob);
    a.download = "token.json";
    a.click();
    URL.revokeObjectURL(a.href);
    setStatus("ok", "Downloaded token.json", "SUCCESS");
  });

  ui.btnCloseModal.addEventListener("click", hideModal);

  ui.modalBackdrop.addEventListener("click", (e) => {
    if (e.target === ui.modalBackdrop) hideModal();
  });

  document.addEventListener("keydown", (e) => {
    if (e.key === "Escape") hideModal();
  });

  ui.tabViewer.addEventListener("click", () => setTab("viewer"));
  ui.tabJson.addEventListener("click", () => setTab("json"));

  // Initial state
  setLink("");
  setCodes({ userCode: "", deviceCode: "" });
  setStatus("idle", "Click Login to start.", "IDLE");
  setTab("viewer");
})();
</script>
</body>
</html>`;
}

使用方法:


📌 转载信息
转载时间:
2026/1/15 18:19:11

Claude Code Workflow (CCW)


Claude Code Workflow (CCW) 是一个 JSON 驱动的多智能体开发框架,具有智能 CLI 编排(Gemini/Qwen/Codex)、上下文优先架构和自动化工作流执行。它将 AI 开发从简单的提示词链接转变为一个强大的编排系统。

项目地址:
catlog22/Claude-Code-Workflow

安装方式:

npm install -g claude-code-workflow ccw install #安装工作流 ccw view #打开看板


CCW Issue Loop 工作流(需要搭配 ACE tools)

什么是 Issue Loop 工作流

Issue Loop 是 CCW (Claude Code Workflow) 中的批量问题处理工作流,专为处理项目迭代过程中积累的多个问题而设计。与单次修复不同,Issue Loop 采用 “积累 → 规划 → 队列 → 执行” 的模式,实现问题的批量发现和集中解决。


两阶段生命周期

Phase 1: 积累阶段

在项目正常迭代过程中,持续发现和记录问题:

・任务完成后 Review → /issue:discover → 自动分析代码发现潜在问题
・代码审查发现 → /issue:new → 手动创建结构化 Issue
・测试失败 → /issue:discover-by-prompt → 根据描述创建 Issue
・用户反馈 → /issue:new → 手动录入反馈问题

Phase 2: 批量解决阶段

积累足够 Issue 后,集中处理:

Step 1: /issue:plan --all-pending # 为所有待处理 Issue 生成解决方案 Step 2: /issue:queue # 形成执行队列(冲突检测 + 排序) Step 3: /issue:execute # 批量执行(串行或并行)

Issue 状态流转

registered → planned → queued → executing → completed


命令详解

Claude 命令

/issue:new — 根据描述注册 Issue
/issue:discover — 多个视角自动分析代码发现问题
/issue:discover-by-prompt — 根据问题(bug,需求)深入探索发现 Issue
/issue:plan — 为 Issue 生成解决方案
/issue:queue — 用于解决冲突,复用上下文,形成执行队列,可划分多个独立队列
/issue:execute — 执行队列中的解决方案(Claude 作为协调中枢支持 agent,Codex 并行执行)

Codex 命令

/prompt:issue-execute — 在 Codex 串行执行队列中的解决方案,支持 queue 指定,工作树隔离 (实测无中断,理论无限时长,当前合计最多跑了 1.5 天,晚上断网~~)


可视化

通过看板(ccw view 启动)可以查看 issue 状态及队列状态


使用场景

下面是个简单的使用流程:

1. 完成 功能开发 2. 执行 /issue:discover 发现技术债务 3. 执行 /issue:plan --all-pending 4. 使用 /issue:queue 形成队列 5. 使用 codex 执行 /prompt:issue-execute 批量处理


技巧

・在有充足的上下文的时候(开发途中,任务完成),使用 CLI 去提需求,生成 Issue 清单,然后再 recover 对话。
・可以将任务完成产物扔给 /issue:new 快速产出测试规划以及需求扩展。


完善中

・可视化界面队列管理,拼接,增强多人协作


下贴预告


— 全文完 (采用 CCW text-formatter skill 进行格式化) —


📌 转载信息
转载时间:
2026/1/15 18:19:05

之前一直没有体验 codex,最近空下来了,我来一个一个 cli 工具拷打一下,接入我真实的开发任务。一看这个 Codex 读写文件的原理我 TM 头的大了。他使用指令读取文件的时候,居然不携带我的 profile.ps1。网上那篇点击率贼高的文章没个用。

我系统是
Microsoft Windows 10 专业工作站版
版本 10.0.19045 内部版本 19045

powershell 版本:

PS C:\Users\Administrator> $PSVersionTable Name Value ---- ----- PSVersion 5.1.19041.5965 PSEdition Desktop PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
BuildVersion 10.0.19041.5965 CLRVersion 4.0. WSManStackVersion 3.0 PSRemotingProtocolVersion 2.3 SerializationVersion 1.1.0.1 

原理也没什么好说的,就是 codex 调用工具的时候告诉他要用 utf8 读。

好像直接使用编辑器贴 Skill 有问题我就留下个 git 链接吧。

也不用点 Star,我很社恐…

Zxyy-mo/Get-It-Done: 没什么大道理,全是自用的 LLM 实操小技巧。记录平时用 OpenAI 和 Claude 时比较顺手的提示词、Skills、插件配置,以及一些摸索出来的 Vibe Coding 瞎搞经验


📌 转载信息
原作者:
wangziwen
转载时间:
2026/1/15 18:18:01

看到大家都在折腾 Claude,我也来分享一下我自己目前在用的 Skills 清单。主打实用,涵盖前端、SEO 和 提示词生成。

常用 Skills 清单

1. Anthropic 官方 Skills
官方出品,必属精品。这里面我只安装了 skill-creator,用来手搓其他 Skill 非常方便。

https://github.com/anthropics/skills

2. UI UX Pro Max
前端神器,用来写 UI 非常好用,推荐前端佬们试试。

https://github.com/nextlevelbuilder/ui-ux-pro-max-skill

3. seo-review
主要用来做 AI 网站的 SEO 审查,查漏补缺。

https://github.com/leonardomso/33-js-concepts/tree/master/.opencode/skill/seo-review

4. content-creator
SEO 的好搭档。根据关键词自动创建博客文章,适合做内容营销。

https://github.com/davila7/claude-code-templates/tree/main/cli-tool/components/skills/business-marketing/content-creator

5. skill-prompt-generator
主要用于生成提示词,搭配生图工具使用效果拔群,强烈推荐!

https://github.com/huangserva/skill-prompt-generator

6. Planning-with-files
参考了 Manus 的 Agent 思路写的 Skill。非常适合处理多步骤的复杂任务。

https://github.com/OthmanAdi/planning-with-files


个人心得与建议

最后强烈建议各位佬友,一定要多多搭建属于自己的 Skills ,从日常的一些工作流开始沉淀。

我的 Skill 迭代方法论:

  1. 手动跑通: 遇到流程,先用 Claude Code (CC) 人肉跑一遍。

  2. 总结提炼: 跑通后,让 CC 总结成一个 Skill。

  3. 评分优化: 基于第一性原理进行打分,不断迭代调优。

如果觉得手写代码麻烦,可以直接用官方的 skill-creator 进行自然语言沟通,创建一个自己的 Skill。

自己沉淀下来的 Skill,才是最好的生产力工具。



📌 转载信息
原作者:
Hiven09
转载时间:
2026/1/15 18:17:35

前言

用电脑的时候,经常需要翻译一下,比如下载了个英文软件,英文又不怎么好。
十年前那会经常用一个叫做天若 ocr 的软件,遇到看不懂的,直接截图识别再翻译一下,特别方便。后来不知道怎么的就不能用了。
中间试了多个软件,都不怎么如意,或者说没有天若那个纯粹。

正文

推荐一个叫做 pot 的软件,它很纯粹,专门用于这个应用场景 (截图翻译)。

它的问题就是识别很烂,乱七八糟得,英文中文符号大锅烩,根本看不懂。但是它可以安装插件,我安装了个 RapidOCR,就可以正常识别了。

它还有个问题就是开始开启 hdr 的话,截图界面就像是加了滤镜一般,灰蒙蒙地,灰色的字都会消失,这个我研究了半天没找到解决方案,所以干脆关闭了 hdr。

做了这番配置,使用体验就很好了,和当年得天若没啥区别了。


📌 转载信息
转载时间:
2026/1/15 18:17:19

各位 L 站的佬友们好!

我是本坛萌新,潜水有一段时间了,一直在这个技术氛围浓厚的社区里学习。今天终于鼓起勇气发个贴,分享一个自己最近开发的练手工具,希望各位佬轻喷,也欢迎大家多提宝贵意见!

开发初衷

平时用夸克网盘比较多,但官方客户端的广告和臃肿大家都懂。加上我有自动化整理资源的需求,官方缺少 API 支持。
作为一名开发者,手痒之下就用 Go (Wails) + Vue3 + Element Plus 撸了这个第三方客户端。

目前项目已经打包了 Windows 版本(单文件绿色版),主打一个干净、无广、可编程,发出来分享给有需要的坛友们体验。

[软件分享] QuarkManager - 萌新首作,基于 Wails + Vue3 打造的夸克网盘桌面端1 [软件分享] QuarkManager - 萌新首作,基于 Wails + Vue3 打造的夸克网盘桌面端2 [软件分享] QuarkManager - 萌新首作,基于 Wails + Vue3 打造的夸克网盘桌面端3 [软件分享] QuarkManager - 萌新首作,基于 Wails + Vue3 打造的夸克网盘桌面端4

界面预览





核心亮点

除了基础的文件管理(上传 / 下载 / 重命名 / 移动等),针对像我这样的 “折腾党”,做了以下增强:

  • 多账户无缝切换:支持同时登录多个账号,一键切换,适合多号党。
  • 内置 API 服务:自带 Swagger 文档的 HTTP API(默认 8080 端口),支持外部程序调用,方便自己写脚本对接网盘。
  • Cookie 过期提醒:支持配置 SMTP 邮件通知,Cookie 快过期时自动发邮件提醒,避免自动任务挂掉。
  • 强大的分享与转存:支持批量转存带密码的链接,自动解析,支持设置分享有效期。

技术栈分享

虽然目前代码还在整理中暂未开源,但还是想和大家交流一下技术选型。
Wails 的方案在 Windows 下表现真的非常不错,利用系统自带的 WebView2,体积比 Electron 小很多,内存占用也低,Go 写后端逻辑也很舒服。

  • Backend: Go (Wails 框架)
  • Frontend: Vue 3 + Element Plus
  • API: 内置 HTTP Server + Swagger

进阶玩法:API 调用

这是我个人最喜欢的功能,开启 API 服务后,你可以直接用 curl 或者 python 操作网盘,做一些自动化的事情:

# 1. 获取文件列表
curl "http://localhost:8080/api/files?folder_id=0" # 2. 一键转存分享链接
curl -X POST http://localhost:8080/api/save \
  -H "Content-Type: application/json" \
  -d '{"share_url": "[https://pan.quark.cn/s/xxx](https://pan.quark.cn/s/xxx)"}'

访问 http://localhost:8080/swagger/ 还能看到完整的接口文档。

📥 下载与安装
目前 Release 页面已上传编译好的 quarkpan.exe,下载解压即可直接运行。

GitHub 发布页: https://github.com/dpyyds/QuarkManager/releases

⚠️ 免责声明
本软件为第三方个人开发工具,仅供学习交流,严禁用于商业用途。

使用第三方客户端可能存在风险,请大家自行评估,后果自负。

如涉及侵权请联系删除。

初来乍到,希望能融入 L 站这个大家庭。如果觉得这个小工具好用,求各位佬去 GitHub 点个 Star 🌟 支持一下,也欢迎在评论区交流 bug 和建议!

📌 转载信息
转载时间:
2026/1/15 18:15:48

最近已经用 OpenCode 搭配 Oh-My-OpenCode 替换 CC 了
在源码中找到 100 个小技巧

输入与文件操作

  1. 输入 @ 后跟文件名可模糊搜索并附加文件
  2. 以!开头可直接运行 shell 命令(如!ls -la)
  3. 拖放图片到终端可添加为上下文
  4. Ctrl+V 从剪贴板粘贴图片到提示框
  5. Ctrl+X E 或 /editor 在外部编辑器中编写消息
  6. Shift+Enter 或 Ctrl+J 在提示中添加换行
  7. Ctrl+C 清空输入框
  8. Escape 中途停止 AI 响应

Agent 与模型

  1. Tab 在 Build 和 Plan agent 之间切换
  2. 切换到 Plan agent 可获得建议而不实际修改
  3. 使用 @agent-name 在提示中调用专用子 agent
  4. F2 快速切换最近使用的模型
  5. /models 或 Ctrl+X M 查看和切换可用 AI 模型
  6. /connect 添加 75+ 支持的 LLM 提供商的 API key
  7. 使用 /connect 连接 OpenCode Zen 获取精选模型

会话管理

  1. /undo 撤销最后的消息和文件更改
  2. /redo 恢复之前撤销的消息和文件更改
  3. /share 创建对话的公开链接
  4. /unshare 取消会话的公开访问
  5. Ctrl+X N 或 /new 开始新会话
  6. /sessions 或 Ctrl+X L 列出并继续之前的对话
  7. /compact 在接近上下文限制时总结长会话
  8. Ctrl+X X 或 /export 将对话保存为 Markdown
  9. Ctrl+X Y 复制助手的最后一条消息到剪贴板
  10. Ctrl+X Right/Left 在父子会话间切换
  11. /rename 重命名当前会话
  12. Ctrl+X G 或 /timeline 跳转到特定消息

界面导航

  1. Ctrl+P 查看所有可用操作和命令
  2. Leader 键是 Ctrl+X,与其他键组合可快速操作
  3. Ctrl+X B 显示 / 隐藏侧边栏
  4. PageUp/PageDown 浏览对话历史
  5. Ctrl+G 或 Home 跳转到对话开头
  6. Ctrl+Alt+G 或 End 跳转到最新消息
  7. /theme 或 Ctrl+X T 在 50+ 内置主题间切换
  8. /init 根据代码库结构自动生成项目规则
  9. Ctrl+X H 切换消息中代码块的可见性
  10. Ctrl+X S 或 /status 查看系统状态信息
  11. 启用 tui.scroll_acceleration 获得 macOS 风格平滑滚动
  12. 通过命令面板 (Ctrl+P) 切换用户名显示
  13. /help 或 Ctrl+X H 显示帮助对话框
  14. /details 切换工具执行详情可见性
  15. Ctrl+Z 挂起终端返回 shell
  16. /review 审查未提交的更改、分支或 PR

配置文件

  1. 在项目根目录创建 opencode.json 进行项目特定设置
  2. 在~/.config/opencode/opencode.json 放置全局配置
  3. 添加 $schema 到配置以在编辑器中获得自动补全
  4. 配置 model 设置默认模型
  5. 通过 keybinds 部分覆盖任何快捷键
  6. 将快捷键设为 none 完全禁用它
  7. 在 mcp 配置部分配置本地或远程 MCP 服务器
  8. OpenCode 自动处理需要认证的远程 MCP 服务器的 OAuth
  9. 使用 {env:VAR_NAME} 语法在配置中引用环境变量
  10. 使用 {file:path} 在配置值中包含文件内容
  11. 使用 instructions 在配置中加载额外的规则文件
  12. 设置 agent temperature 从 0.0(专注)到 1.0(创意)
  13. 配置 maxSteps 限制每个请求的 agentic 迭代次数
  14. 设置 “tools”: {“bash”: false} 禁用特定工具
  15. 设置 “mcp_*”: false 禁用 MCP 服务器的所有工具
  16. 为每个 agent 配置覆盖全局工具设置
  17. 设置 “share”: “auto” 自动共享所有会话
  18. 设置 “share”: “disabled” 阻止任何会话共享
  19. 使用 “theme”: “system” 匹配终端颜色

自定义命令与 Agent

  1. 在 .opencode/command/ 添加 .md 文件定义可重用自定义提示
  2. 在自定义命令中使用 $ARGUMENTS、$1、$2 进行动态输入
  3. 在命令中使用反引号注入 shell 输出(如 git status
  4. 在 .opencode/agent/ 添加 .md 文件创建专用 AI 角色
  5. 为 edit、bash 和 webfetch 工具配置每个 agent 的权限
  6. 使用 “git *”: “allow” 模式进行细粒度 bash 权限
  7. 设置 “rm -rf *”: “deny” 阻止破坏性命令
  8. 配置 “git push”: “ask” 在推送前要求批准
  9. 运行 opencode agent create 进行引导式 agent 创建

格式化与 LSP

  1. OpenCode 使用 prettier、gofmt、ruff 等自动格式化文件
  2. 在配置中设置 “formatter”: false 禁用所有自动格式化
  3. 在配置中定义带文件扩展名的自定义格式化命令
  4. OpenCode 使用 LSP 服务器进行智能代码分析

工具与插件

  1. 在 .opencode/tool/ 创建 .ts 文件定义新的 LLM 工具
  2. 工具定义可以调用 Python、Go 等编写的脚本
  3. 在 .opencode/plugin/ 添加 .ts 文件创建事件钩子
  4. 使用插件在会话完成时发送系统通知
  5. 创建插件阻止 OpenCode 读取敏感文件

CLI 使用

  1. opencode run 用于非交互式脚本
  2. opencode run --continue 恢复上一个会话
  3. opencode run -f file.ts 通过 CLI 附加文件
  4. –format json 用于脚本中的机器可读输出
  5. opencode serve 用于无头 API 访问 OpenCode
  6. opencode run --attach 连接到运行中的服务器
  7. opencode upgrade 更新到最新版本
  8. opencode auth list 查看所有配置的提供商
  9. opencode debug config 排查配置问题
  10. –print-logs 标志在 stderr 中查看详细日志

GitHub 集成

  1. 在 GitHub issues/PRs 中使用 /opencode 触发 AI 操作
  2. opencode github install 设置 GitHub workflow
  3. 在 issues 上评论 /opencode fix this 自动创建 PR
  4. 在 PR 代码行上评论 /oc 进行针对性代码审查
  5. 提交项目的 AGENTS.md 文件到 Git 供团队共享

主题

  1. 在 .opencode/themes/ 目录创建 JSON 主题文件
  2. 主题支持深色 / 浅色变体
  3. 在自定义主题中引用 ANSI 颜色 0-255

权限

  1. doom_loop 权限防止无限工具调用循环
  2. external_directory 权限保护项目外的文件

容器化

  1. 运行 docker run -it --rm Package opencode · GitHub 使用容器化版本


来源


📌 转载信息
原作者:
chowxiaodi
转载时间:
2026/1/15 18:15:08