包含关键字 typecho 的文章

问题:原全球区卡是德国地址证明 更改其他地区后原卡被冻 新卡一直无限循环且只能选择德国
解决办法:先更改其他新的地址证明 -> 然后用你新地区的 IP 联系客服 -> 说明卡被冻 -> 让客服提交工单删除 / 或自己提交工单.-> 会给你发邮件 -> 回复 本人知悉并同意 Bybit 卡的取消为不可逆操作。请协助取消我目前的 EEA Bybit Card,并允许我重新申请其他可用的卡片类型。-> 完美解决 -> 再申请新卡就可以了


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

用 Claude Code 跑自动化测试时遇到过这种情况吗?生成了一堆测试用例,跑完发现 Token 烧了不少,结果一半是废的 —— 要么断言写错,要么压根定位不到元素。

所以我花了点时间把市面上的 AI 测试方案摸了一遍。就两条路,DOM 解析或者视觉识别,先选一个深耕就行。


DOM

原理没什么花活。DOM 就是浏览器把 HTML 页面变成一棵可以被 JavaScript 随意增删改查的树状对象结构。简单说,你写的 HTML 被浏览器变成了一堆 "会动的积木",这些积木可以用 JavaScript 随便抓、改、删、加。

AI 通过选择器找到这些积木,Playwright 负责点点点。就这样。

推荐工具:agent-browser

这是 Vercel 出的一个专门用于 AI Agent 的浏览器自动化 CLI,支持无头模式,可以不打开浏览器进行测试。

# 安装 CLI
npm install -g agent-browser

# 安装驱动
agent-browser install

# 验证安装
agent-browser open baidu.com

# 返回以下内容表示成功 # ✓ 百度一下,你就知道 #   https://www.baidu.com/ 

这条路的好处很直接 ——Token 消耗低,跑得快,CI/CD 接进去顺滑。

但有个前提:你的 DOM 结构得稳定。

纯视觉

这个思路更直观:将页面截图发送给视觉大模型识别,模型返回下一步操作指令,循环执行直到完成全部任务。

推荐工具:Midscene

这是字节开源的纯视觉测试方案,支持 Web、Android、iOS,兼容多种视觉模型:Gemini、Qwen-VL、Doubao-VL。自然语言写测试目标,AI 自己生成脚本。

但 Token 消耗高。跑一轮测试下来成本不低。而且视觉模型偶尔会出问题。

所以视觉方案更像是补充:DOM 搞不定的场景,比如验证 UI 有没有错位、样式有没有问题,再让视觉上。

ChatGPT AgentMode

这是 ChatGPT 的 AgentMode 功能需要订阅企业版或者 Pro 版本 (team 也可以) 它会启动一个虚拟浏览器访问你要求的网址像真人一样在你的平台浏览、点击按钮、账号登录 ,要求最后给我详细的测试报告,如果你的网站已经部署到公网上那么就可以使用 Agent Mode 进行测试。


缺点是最少需要购买 Team 或者 Pro。

怎么选?

场景建议
页面结构稳定,预算有限DOM + Playwright
页面动效多、结构不稳定视觉方案
想检测布局错位、样式问题必须视觉,DOM 看不出来
成本敏感DOM 为主

几个坑提前说

成本控制:先让 AI 生成用例,人审完再跑。不然跑完发现一半用例有问题,Token 白烧了。

CI/CD 集成:配置的时候注意超时设置。视觉方案跑得慢,默认超时可能不够。

自愈能力:市面上很多工具都说支持 "自愈",意思是 UI 改了之后测试脚本能自动调整选择器。实际效果看情况,改个按钮文案能自愈,重构了页面结构还是得手动改。


核心就一句:DOM 打底,视觉补充,别一开始就 all in 最贵的方案。

有实践经验的欢迎评论区交流。


相关链接


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

在跟谷歌善人斗智斗勇了一俩个月,看了很多佬友分享,注册了 5、6 个账号之后,我觉得可以总结出一个暴论

  • 时间才是唯一重要的

我是怎么都没想到最后是我最老的一个号获得了资格,这个号 10、11 月注册的,Chrome 和手机上都登录的这个,平常使用 ip 之混乱:大陆、TW、SG、US 各种乱跳。我都不对这个号抱有希望,用美国家宽(风佬和宝可梦)各种渠道注册了若干新号是一个资格都没有,还挑了个号天天刷 YouTube,也是无济于事。直到某一天:

再结合之前有佬友分享的:有一个月体验的基本都有资格

我连忙拉起风佬家宽(是的我发现的时候用的某一元机场)去改地区,把原来 SG 改成 US

我之前申请过一次改美国还被拒了

改成功之后考试去了

期间我一直用的是美国 ip 使用 gemini 这些,家宽机场混着用。

回来后再把原来的付款资料删除,直接拿下资格:

后续就是 sheerid 了,1key 大法好,不过现在需要付费了,但是根本没有库存

于是找佬友 @Oregon 花了点 LDC 帮忙给过了

最后是绑卡,这里直接用之前办的招商万事达

顺利结束

希望能对佬友们有帮助


📌 转载信息
原作者:
LN001
转载时间:
2026/1/15 18:34:43

各中转站对 RPM 有限制,同时禁止分发,本教程只给出自用模式

看到群里不少佬友想在 newapi 中对接中转站,恰好我在用 RightCode,所以以 rightcode 为例,写(水)一篇教程吧。

前者要求

  • git (用于克隆仓库)
  • windows docker desktop 或者 linux docker (建议有足够的内存 + 硬盘存储)
  • 配置好 docker compose

windows docker desktop 建议选择 wsl2 作为 backend

安装部署

克隆仓库 & 启动

先克隆仓库

git clone https://github.com/QuantumNous/new-api.git

修改 docker-compose.yml

本教程采用 postgresql

```diff
version: '3.4' # For compatibility with older Docker versions

services:
  new-api:
    image: calciumion/new-api:latest
    container_name: new-api
    restart: always
    command: --log-dir /app/logs
    ports:
-     - "3000:3000" +     - "3003:3000"
    volumes:
      - ./data:/data
      - ./logs:/app/logs
    environment:
-     - SQL_DSN=postgresql://root:123456@postgres:5432/new-api # ⚠️ IMPORTANT: Change the password in production! +     - SQL_DSN=postgresql://root:idkpassword@postgres:5432/new-api # ⚠️ IMPORTANT: Change the password in production!
#       - SQL_DSN=root:123456@tcp(mysql:3306)/new-api  # Point to the mysql service, uncomment if using MySQL
      - REDIS_CONN_STRING=redis://redis
      - TZ=Asia/Shanghai
      - ERROR_LOG_ENABLED=true # 是否启用错误日志记录 (Whether to enable error log recording)
      - BATCH_UPDATE_ENABLED=true  # 是否启用批量更新 (Whether to enable batch update)

    depends_on:
      - redis
      - postgres
    healthcheck:
      test: ["CMD-SHELL", "wget -q -O - http://localhost:3000/api/status | grep -o '\"success\":\\s*true' || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3

  redis:
    image: redis:latest
    container_name: redis
    restart: always

  postgres:
    image: postgres:15
    container_name: postgres
    restart: always
    environment:
      POSTGRES_USER: root
-     POSTGRES_PASSWORD: 123456  # ⚠️ IMPORTANT: Change this password in production! +     POSTGRES_PASSWORD: idkpassword  # ⚠️ IMPORTANT: Change this password in production!
      POSTGRES_DB: new-api
    volumes:
      - pg_data:/var/lib/postgresql/data

volumes:
  pg_data:
#  mysql_data:

编辑完保存,继续执行命令

docker compose up -d

等待 n 秒(取决于你的网速~)

出现以下字样,拉去镜像和启动成功

[+] Running 5/5
 ✔ Network new-api_default   Created                                                                                                                   0.1s
 ✔ Volume "new-api_pg_data"  Created                                                                                                                   0.0s
 ✔ Container redis           Started                                                                                                                   0.7s
 ✔ Container postgres        Started                                                                                                                   0.7s
 ✔ Container new-api         Started                                                                                                                   1.0s

NewAPI 配置

打开浏览器,输入 http://localhost:3003/ 后,会出现配置页

如果你的数据库检查没有错误,继续下一步,填写管理员账号和密码

下一步,选择使用模式

最后 初始化系统 即可

配置渠道

打开 控制台

依次点击 渠道管理 添加渠道,并填入 类型 / 名称 / 密钥

填入 API 地址为 https://www.right.codes/codex

因为 rightcode 支持了模型列表接口,点获取模型列表即可获取可用的模型

随后确定,并提交即可

模型管理

按照图中的内容,切换刀模型管理,依次点击 同步 → 下一步 → 确定

配合 CC Switch 使用

现在 控制台 -> 令牌管理 生成令牌,并填入刀 cc switch 中

最终的 config.toml

model_provider = "custom" model = "gpt-5.2" model_reasoning_effort = "xhigh" disable_response_storage = true [model_providers.custom] name = "custom" wire_api = "responses" requires_openai_auth = false base_url = "http://localhost:3003/v1" 

保存好以后,切换供应商,可以开始 coding 了


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

这个开源项目精选了超级多的 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

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

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

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

各位 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

直接安装: app.lingkuma.org

Youtube 字幕使用教程

Tip

电脑端和安卓端 lingkuma 已经兼容原版 trancy 。
手机端必须使用内置的 lingkuma trancy,因为 safari 似乎有些问题,导致无法在 trancy 的元素上做高亮等。如果你知道如何修改请在 issues 中分享,万分感谢。

沉浸式字幕(电脑 / 手机支持)

  1. 手动选择目标语言的字幕,并启动默认字幕。

    • 更换语言,翻译等操作请在默认的字幕设置内更改,然后启动即可。
  2. 点击右侧快捷开关后即可启动

内嵌式字幕

  1. popup 中打开嵌入式字幕选项,然后刷新页面

📌 转载信息
原作者:
Claude4
转载时间:
2026/1/15 18:14:56

分享一套自研的 AI Agent 开发方法论,通过 Obsidian 管理 Spec 文档,让 AI 严格按照设计文档执行开发。核心思路很简单:

自己可能有个对系统的优化想法,通过 intent-confirmation 和 AI 对齐一下,你描述的可能不是很清楚,AI 通过这个 SKill 优化成专业一点的表达,同时确定修改范围,你觉着没问题了就进入了下面的 Spec 驱动开发流程:

先写 Spec(spec-writer)→ 用户确认 → AI 按文档执行(spec-executor)→ 审查是否符合 Spec(spec-reviewer)→ 完成归档。整个流程完全可追溯,每个功能都能追溯到 Spec 文档的具体章。

基于 MUSE 框架实现了三层记忆架构,让 AI 能够长期学习和积累经验:战略记忆存储项目级的「困境 - 策略」对,程序记忆存储可复用的标准操作流程(SOP),工具记忆记录操作完成后的固定步骤。

目前这套体系包含 12 个 Skills,覆盖了 Spec 创建、执行、更新、审查的完整闭环,还集成了 Obsidian 的双链、元数据、Callout 等特性来实现文档关联和动态索引。

GitHub 已开源,欢迎交流:GitHub - HHU3637kr/skills: 自用的一些 skills


📌 转载信息
原作者:
HHU3637kr
转载时间:
2026/1/15 18:14:11