很多读者都会好奇少数派的编辑们到底平时都「买了啥」。我们希望通过「编辑部的新玩意」介绍编辑部成员们最近在用的新奇产品,让他们自己来谈谈这些新玩意的使用体验究竟如何。

内容声明:《新玩意》栏目如含有商务内容,将会在对应条目标注「广告」。


@张奕源 Nick:

拓竹 P2S 3D 打印机 + AMS 2 Pro

  • 参考价格:¥3994.15(含 AMS,价格为政府补贴后)

前文有提到,我因为想搞家庭收纳而入坑了 3D 打印,所以在家里摆了一台拓竹的 P2S。

P2S 虽然已经发布了两个多月,但依然算是拓竹的新品。得益于我入坑晚,没经历过 3D 打印在民用化、小型化过程中的历次迭代,一上手就是几乎没有缺陷的成熟产品,所以 P2S 在我眼里已经趋近完美,在使用它的这两周里,我收获的全是新奇感。

首先,它很易用,非常非常容易上手。你可能也对他们的开源模型社区 Makerworld 有所耳闻,这上面有各种各样花里胡哨、稀奇古怪的模型可供下载,而且其中的大部分都针对拓竹打印机写好了配置,只需要下载之后跟自己的机型同步一下,就能直接开打,不用修改任何参数。

而且 Makerworld 上有很多强工具向的冷门模型,恰好满足了我的需要,譬如我在筹备我派的《寻源南疆》项目拍摄,要带一堆拍摄工具,我就打了一套索尼相机的电池盒、镜头盖等。还有一些收纳盒,也都是针对 U 盘之类的小玩意设计的,很适合旅途携带,实用又方便。

3D 打印的卡口盖和电池盒,这类玩意单买不划算,很适合 3D 打印

如果你和我一样是特别怕麻烦的超级懒人,那可以在购机的时候顺便整一套 AMS(或者直接买套装),耗材也直接用拓竹第一方的。这样一来,机器可以自动读取颜色、余量等信息,上料、退料、换料也都自动完成,几乎可以做到「除了要自己决定想打啥,别的都能撒手不管」,看上什么模型打就完了。

此外,它的易用性还体现在对打印机的摆放环境要求没那么严格。我家里地方小,没有多余空间再摆放很沉重、稳定的桌子了,只好把 P2S 放在厨房一个三脚茶桌上。我本来还担心晃动会影响打印质量,结果完全没事。后来我研究了一下才发现,P2S 有一套内置的平衡补偿机制,对于小幅度的桌子晃动之类都能自动找平和稳定,没有我担心得那么娇气。

其次,它的出品很稳定。我用过的 3D 打印机不多,P2S 肯定是其中出品质量最高的那个。在默认状态下,P2S 打出来的模型就已经足够细腻厚实,而且打印过程几乎没有出过大错。我唯一一次遇到炒面的情况还是模型设计本身的问题。通常来说,打印机的配套 app《Bambu Studio》会自动判断和处理模型是否需要加支撑、加边之类,选择模型时也可以稍微看看大家晒出的成品或者反馈的打印质量,基本就能避免类似的情况。

滑盖收纳盒咬合得很到位,紫色的料用完之后续上粉红的接着打,融合得也不错

再次,它的运行噪音很低,这一点超出了我的预期。P2S 采用了全封机身,能有效隔绝大量的噪音,加上它打印时主要发出的是一种持续、中低频的声音,所以不算恼人,响度大体和正在运行中的空调接近。我一开始是把它放在客厅、挨着我的办公桌的,边打印边工作都没什么大碍。但考虑到我之后会利用睡觉时间打长任务,所以还是把它放进了厨房,工作期间在卧室完全听不到声音,不打扰休息。

再推几个我最近比较喜欢的模型吧,如果你也有 3D 打印机可以打打看。

第一个是我目前最喜欢的收纳盒,「机械风格收纳盒」的单色版本。它有方方正正的盒体,所以更便于摆放;默认的尺寸就很合适,容量大,好收纳;支援堆叠,有位置合理的卡槽,可以无限叠叠乐。这个盒子我打了得有十个,把家里的各种乱七八遭的小东西都收纳了一遍,相当解压。

模型地址:https://makerworld.com.cn/zh/models/1018417-ji-jie-feng-ge-shou-na-he-ke-dui-die

超好用!

第二个是「BUCKETS – 可堆叠收纳盒」,这款是在所有我打过的斜口收纳盒里品质最好也最好看的。它有厚实的外壁和大收纳空间,也做了可堆叠设计,而且这种窄瘦的造型更适合放在边边角角里,装什么都行,很百搭。

模型地址:https://makerworld.com.cn/zh/models/1504594-buckets-ke-dui-die-shou-na-he-wu-xu-zhi-cheng

也很好用!

第三个是个偏冷门的「带滑盖的大卡片盒」,这玩意原本是个塔罗盒,滑盖上的图案也是按着这个路子来的。但这个盒容量大,工艺精致,打印时间还不长,是我目前打过的滑盖盒里综合品质最高的。

模型地址:https://makerworld.com.cn/zh/models/631847-dai-hua-gai-de-da-qia-pian-he

非常细腻还不费料,适合用这种木质材料

MelGeek 蜜氪奇点 Centauri 60 磁轴键盘

  • 参考价格:¥1695.18(首发优惠价)

买这块键盘有一半是冲动消费。它长得挺好看,60% 配列的布局很紧凑,磁轴的手感我也很好奇,所以我在产品刚发布的时候就下单了,也算是当了一把第一批用户。

好在这次冲动没有受到惩罚——奇点 60 好用。它默认配的是「TTC 反斗万磁王白轴」,手感其实和茶轴类似,都是直来直去、清脆不累的手感。我用它基本都是打字办公,长期使用也不会觉得累,而且因为手感酥脆,所以很容易进入某种心流状态,蛮好玩的。

拓展阅读:https://sspai.com/post/105108

奇点 Centauri 系列还有一个更旗舰的 80 款,区别在于使用了 80% 配列,有 F1-F12 功能键区,而且键盘右侧多了一块萤幕,可以查看键盘状态或者调整参数。我更习惯键盘靠近鼠标,希望键盘越小越好,所以就没买大的。而且 60% 配列也让键盘整体更显紧凑,我觉得比 80 好看一些。

我还很喜欢的奇点 60 的 LED 灯带,MelGeek 为它专门做了一个类似贪吃蛇绕圈圈的光亮效果,这让键盘有了些许趣味,而且比 RGB 闪瞎眼高级很多。

至于键盘性能,我反而没太操心。咱毕竟不打 CS 多年,对键盘的延迟、无冲等指标已经没有太多追求。MelGeek 提供的网页版驱动也覆盖了完整的参数调节选项,不仅可以逐键定义,还能照抄职业选手的配置,搞起来十分简单。但其实这把键盘是支援从里到外完全自定义的,从轴体到底棉,再到板簧和定位版,甚至键盘外部的金属装饰框,理论上都能随便更换或者调整。对于喜欢折腾的玩家来说,这肯定是个好消息。

MelGeek 的驱动介面,可以单独调整每个键的参数,也可以直接抄作业

不过,我对奇点 60 也有不满意之处——它的灯效速率太快了,即便是调到最慢也要大概一秒变换一次,做不出那种缓亮缓灭的呼吸感。这其实是个软体层面可以解决的问题,如果 MelGeek 的朋友能看到这篇文章,希望可以考虑再加几个灯光速度的档位,照顾一下我们这些老年人 :)

@克莱德:米家标签打印机

  • 参考价格:¥139(带 3 卷标签纸)

每到换季都要翻箱倒柜找衣物和床上用品,每年也都会一遍遍重复那个永恒不变的自我拷问:顶上那个收纳盒里放的什么东西来着?

今年索性决定购入一台标签机,给家里的一切收纳容器都贴上「防呆标记」。因为是自己此前从未主动了解过的新品类,所以首先找到了生活小电器知名品牌米家。

和以往见过的可以打印各种图片、发票样式的标签机不同,米家这款标签打印机主要面向的是需要「贴贴贴」的场景,所以使用的耗材也是宽度固定的、类似透明胶布的长条状标签纸,在米家 app 内可以手动设置打印标签的固定长度(最多 150mm),也可以根据打印的实际内容自动决定。

标签文本的编辑工作自然也是在米家 app 中完成,应用内提供的排版功能包括样式、字体、对齐,样式中又包含基本的加粗、倾斜、下划线、字间距、行间距、文本方向等,移动文本的过程中会提供辅助参考线,也内置了一些固定的对齐方式和微调方向按钮。如果是日常助记的文本标签,没有太多的排版和设计需求,这些工具基本能够满足——但如果你想多点装饰和趣味,它不支持 emoji 输入、仅提供有限的贴纸图案和内置图文模板,就会显得有些力不从心甚至可以说是非常简陋了。

不过在我的使用场景下,标签纸的打印效果可以说清晰锐利,并且标签纸采用的是中间对半剥开的设计,也可以避免在边角手搓导致边角粘性下降的问题。

唯一的问题是标签纸默认为透明背景+黑色字体,且仅支持黑白打印,所以打印的标签用在一些浅色收纳容器或白色家电、电子产品上效果还行,但如果是深色就有点恼火了——这里你只能牺牲一半的标签宽度、剥开一半的胶面,然后将文本打印在没有剥开的那部分,达到白底黑字的效果。


@PlatyHsu:徕芬 Swift 4 吹风机

  • 参考价格:560(国补后,原价 659)

我一般是不怎么用得到吹风机的——用我妈的话说,你那几根毛有什么好吹的。(澄清:还是有几根的。)不过,人在深圳,你也不知道什么时候就会天赐甘露,还是需要留一手能让自己快速变得体面的方式。

我的上一个吹风机,就是在这样一个雷雨天抱头逃回的路上,花几十块钱从外卖软件上点的。可想而知,它的风力即使对付我那几根毛都有点过于文明了。正好前段时间看国补优惠信息的时候,刷到了徕芬的吹风机,问了一圈周围买过的人,评价都还行,就弄了一个新型号 Swift 4试试。

素闻徕芬擅长致敬苹果,果不其然,从牛皮纸箱和封口方式,到只印了产品图片的白色包装,再到内附的说明文档排版,甚至拆封后的清洁剂气味,无不散发出浓浓的果味。

说回产品本身,Swift 4 这一代相比过往型号,主要是改进了机身材质,用上了铝合金,看起来还是比较精致的。电源线有理线器,不过长度 1.7 米(也就是不到两根手机充电线的长度)稍微短了一点。

工作性能方面,Swift 4 最高风速标称 23m/s,算是比较快的水平;但是因为机身尺寸不大,出风量肯定还是比不上 Tony 老师们拿的那些大家伙,只能说对我是够用了。工作噪音标称最大 59dB,我用手机量了一下差不多,还是比较安静的。搭配不同工作模式,机身尾部的圆形灯光会显示出四种颜色,是一个醒目也好看的设计。

如今什么家电产品都要赶时髦搭配点「智能」,Swift 4 也不能免俗,内置了蓝牙,可以和徕芬 app 或者微信小程序搭配使用。我贫乏的想象力实在无法理解吹风机为什么要智能——更离谱的是还只能电机呜呜转的状态下配对和操作——但好在除了冷热定时循环之外,并没有什么非要配对才能实现的功能,大多数时候直接忽略这部分就好了。

总的来说,Swift 4 在补贴下的性价比还是可以的,虽然可能在一些细节上比起戴森还是有差距,但对我这种偶尔用用的已经足够了。如果非要挑什么毛病的话,简洁精致并不是只有苹果那一种表现形式,也许在学习的时候可以更有创意和自信一些。

 

我们近期开通了新玩意的社媒帐号,更有更多新奇产品和服务以视频方式呈现,快来关注我们吧!

如果你也想分享「新玩意」🔉:

  • 获取 Matrix 社区写作权限并签署 Matrix 共创计划
  • 新发布一篇文章,在标题中标注「新玩意」前缀;
  • 用至少 800 字介绍产品,并配上 2-3 张产品的实拍图片;
  • 在网站个人信息中补充支付宝账号。

成功入选还可以得到 108 元的「剁手红包」🧧,并在每周二的社区速递栏目中展示。如果你有兴趣参与,就赶紧来稿吧!

> 下载少数派 客户端、关注 少数派公众号,了解更多的新玩意 🆒

> 特惠、好用的硬件产品,尽在 少数派 sspai 官方店铺🛒

    在跟谷歌善人斗智斗勇了一俩个月,看了很多佬友分享,注册了 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

    对于 AI 开发过程中,AI coder 最喜欢用的骚紫色,我已经有了 Prompt 规避,分享给各位,大家如果也有其他场景的 Prompt 也可以发送一下:

    #角色
    你是一位资深设计资深前端后端全栈开发工程师 #设计风格
    优雅的极简主义美学与功能的完美平衡;
    永远不使用 AI 盛行的基佬紫色!
    清新柔和的渐变配色与品牌色系浑然一体;
    恰到好处的留白设计;
    轻盈通透的沉浸式体验;
    信息层级通过微妙的阴影过渡与模块化卡片布局清晰呈现;
    用户视线能自然聚焦核心功能;
    精心打磨的圆角;
    细腻的微交互;
    舒适的视觉比例;
    强调色:按 APP 类型选择;


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

    各中转站对 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

    在阅读完成 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