sintbot 1.0.16 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.json +99 -0
- package/CHANGELOG.json +99 -0
- package/README.md +20 -158
- package/README.zh-CN.md +20 -166
- package/THIRD-PARTY-NOTICES.md +394 -0
- package/bin/sintbot-v2.js +363 -0
- package/docs/AI-ASSISTED-INSTALL.md +11 -35
- package/docs/install.md +61 -167
- package/docs/zh-CN/AI-ASSISTED-INSTALL.md +11 -35
- package/docs/zh-CN/install.md +61 -165
- package/install.sh +84 -0
- package/package.json +73 -37
- package/src/agent/claudecode.js +149 -108
- package/src/agent/codexapp.js +458 -0
- package/src/agent/codexcode.js +210 -107
- package/src/agent/errtag.js +34 -0
- package/src/agent/privatecore.js +790 -0
- package/src/agent/privatecore.wasm +0 -0
- package/src/agent/runner.js +10 -8
- package/src/agent/winexec.js +35 -5
- package/src/c2install.js +117 -0
- package/src/config.js +2 -1
- package/src/core/c2cardeffectbridge.js +51 -0
- package/src/core/c2cardterminalpath.js +216 -0
- package/src/core/c2durableingress.js +148 -0
- package/src/core/c2durablejournal.js +1560 -0
- package/src/core/c2durableruntime.js +2361 -0
- package/src/core/c2eventidentity.js +90 -0
- package/src/core/c2globalfault.js +401 -0
- package/src/core/c2localeffectexecutor.js +193 -0
- package/src/core/c2payloadhandles.js +333 -0
- package/src/core/c2persistrelation.js +179 -0
- package/src/core/c2persiststate.js +342 -0
- package/src/core/c2platformrecoverysender.js +76 -0
- package/src/core/c2recoverygate.js +385 -0
- package/src/core/c2runnerinput.js +63 -0
- package/src/core/c2servicestartgate.js +103 -0
- package/src/core/c2sessionbridge.js +72 -0
- package/src/core/c2sourcequarantine.js +30 -0
- package/src/core/c2startuprecovery.js +105 -0
- package/src/core/c2successorbridge.js +151 -0
- package/src/core/c2terminaleffectbridge.js +154 -0
- package/src/core/c2terminalprune.js +108 -0
- package/src/core/c2turnprojection.js +177 -0
- package/src/core/enginecheckpoint.js +211 -0
- package/src/core/engineeffectcursor.js +394 -0
- package/src/core/enginestepcoordinator.js +183 -0
- package/src/core/store.js +52 -8
- package/src/core/v2engine.js +1469 -0
- package/src/platform/feishu.js +324 -67
- package/src/plugins.js +1 -9
- package/assets/design-styles/acid-pop.md +0 -155
- package/assets/design-styles/archive-index.md +0 -161
- package/assets/design-styles/art-deco.md +0 -164
- package/assets/design-styles/cinematic-noir.md +0 -178
- package/assets/design-styles/editorial.md +0 -121
- package/assets/design-styles/extra/README.md +0 -23
- package/assets/design-styles/extra/constructivism.md +0 -146
- package/assets/design-styles/extra/neo-brutalism.md +0 -175
- package/assets/design-styles/extra/new-wave.md +0 -166
- package/assets/design-styles/extra/nordic-material.md +0 -162
- package/assets/design-styles/extra/paris-couture.md +0 -162
- package/assets/design-styles/extra/tropical-modern.md +0 -168
- package/assets/design-styles/extra/ulm-functional.md +0 -182
- package/assets/design-styles/extra/wa-shikimen.md +0 -160
- package/assets/design-styles/extra/y2k-chrome.md +0 -172
- package/assets/design-styles/geometric-pop.md +0 -122
- package/assets/design-styles/hand-drawn.md +0 -117
- package/assets/design-styles/islamic-geometry.md +0 -155
- package/assets/design-styles/liquid-glass.md +0 -183
- package/assets/design-styles/neo-chinese.md +0 -136
- package/assets/design-styles/precision-mono.md +0 -156
- package/assets/design-styles/quiet-luxury.md +0 -163
- package/assets/design-styles/riso-print.md +0 -116
- package/assets/design-styles/soft-friendly.md +0 -174
- package/assets/design-styles/swiss-grid.md +0 -156
- package/assets/design-styles/tech-dark.md +0 -182
- package/assets/favicon.svg +0 -6
- package/assets/logo.svg +0 -24
- package/bin/sbot.js +0 -4716
- package/docs/slack-app-manifest.yaml +0 -104
- package/sbot.config.example.json +0 -235
- package/src/agent/askonce.js +0 -76
- package/src/agent/claudemodels.js +0 -60
- package/src/agent/claudestatus.js +0 -74
- package/src/agent/claudeusage.js +0 -200
- package/src/agent/codexstatus.js +0 -181
- package/src/agent/imagen.js +0 -458
- package/src/agent/quotaerr.js +0 -44
- package/src/agent/transienterr.js +0 -32
- package/src/cdp.js +0 -254
- package/src/cli-secret.js +0 -36
- package/src/clilang.js +0 -31
- package/src/core/cron.js +0 -182
- package/src/core/cronfire.js +0 -120
- package/src/core/cronrun.js +0 -116
- package/src/core/engine.js +0 -2758
- package/src/core/interfaces.js +0 -114
- package/src/core/issues.js +0 -185
- package/src/core/ledger.js +0 -30
- package/src/core/opsalert.js +0 -214
- package/src/core/pending.js +0 -123
- package/src/core/proxy.js +0 -424
- package/src/core/quotaalert.js +0 -79
- package/src/core/quotaerr.js +0 -55
- package/src/core/scheduler.js +0 -59
- package/src/design.js +0 -5161
- package/src/gitrepo.js +0 -150
- package/src/health.js +0 -353
- package/src/installdeps.js +0 -72
- package/src/leftover.js +0 -174
- package/src/platform/boardnotice.js +0 -77
- package/src/platform/boardops.js +0 -36
- package/src/platform/boardwatch.js +0 -447
- package/src/platform/cli.js +0 -45
- package/src/platform/slack.js +0 -1289
- package/src/platform/takeovercard.js +0 -37
- package/src/platform/wecom.js +0 -938
- package/src/platform/wecomcli.js +0 -128
- package/src/platform/wecomgate.js +0 -108
- package/src/platform/wecomname.js +0 -43
- package/src/platform/wecompoll.js +0 -241
- package/src/preview.js +0 -581
- package/src/projectmemory.js +0 -175
- package/src/service.js +0 -317
- package/src/ui/auth.js +0 -31
- package/src/ui/browser.html +0 -392
- package/src/ui/browserroutes.js +0 -234
- package/src/ui/console.html +0 -5526
- package/src/ui/dashboard.js +0 -168
- package/src/ui/feishulogin.js +0 -136
- package/src/ui/server.js +0 -2314
- package/src/ui/sessions.js +0 -78
- package/src/ui/takeover.js +0 -108
- package/src/updater.js +0 -243
- package/src/upgrade.js +0 -35
- package/src/usage.js +0 -144
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
# sbot 的 Slack App manifest —— 在 api.slack.com/apps → Create New App → From an app manifest 粘贴这份
|
|
2
|
-
#
|
|
3
|
-
# ⚠ 两台机器(两台机器)各建一个 App,这份 manifest 用两次、改一下 name 即可。
|
|
4
|
-
# 理由见 docs/SLACK.md「必须一开始就定对的三件事」:Socket Mode 多连接是**随机分发不是广播**,
|
|
5
|
-
# 共用一个 app token 会让消息随机落到其中一台,表现为「时灵时不灵」。
|
|
6
|
-
#
|
|
7
|
-
# ⚠ 建完还要手工做两步(manifest 覆盖不到):
|
|
8
|
-
# 1. Basic Information → App-Level Tokens → Generate → 勾 connections:write → 拿 xapp-
|
|
9
|
-
# 2. OAuth & Permissions → Install to Workspace → 拿 xoxb-
|
|
10
|
-
# 然后把 bot 拉进频道:/invite @sintbot —— 不做这步收不到任何消息。
|
|
11
|
-
#
|
|
12
|
-
# ⚠ **已经建过 app 的,改这份 manifest 之后要重装**(App Manifest 页粘贴 → Save Changes →
|
|
13
|
-
# Reinstall to Workspace)。2026-09-03 这版新增了 `app_home` 与 `app_home_opened`:
|
|
14
|
-
# 不重装的表现是**主页永远空白、日志里一条 `views.publish` 失败** —— 不影响任何一轮任务。
|
|
15
|
-
# ⭐ 反过来,主页的**文案**是 daemon 运行时发布的,改文案不用再动 manifest。
|
|
16
|
-
#
|
|
17
|
-
# 📌 未含 app_mentions:read 是**有意的**:已有 channels:history 就会收到频道每一条消息,
|
|
18
|
-
# 自己匹配 bot user id 即可判断被 @;同时订阅 app_mention 会让 @ 消息投递两次(cc-bot 踩过)。
|
|
19
|
-
# 详见 docs/SLACK.md「scope 与 manifest」。
|
|
20
|
-
|
|
21
|
-
display_information:
|
|
22
|
-
name: sintbot
|
|
23
|
-
description: 从群聊驱动无头编码 agent
|
|
24
|
-
background_color: "#0a0a0a"
|
|
25
|
-
|
|
26
|
-
features:
|
|
27
|
-
bot_user:
|
|
28
|
-
display_name: sintbot
|
|
29
|
-
always_online: false
|
|
30
|
-
# ⭐ App Home 主页 = 群内控制面的第三层入口(前两层 = 线程里的按钮 · `!` 前缀)。
|
|
31
|
-
# 内容由 daemon 运行时 views.publish 上去 ⇒ **改文案不用重装 app**,只有这个开关要。
|
|
32
|
-
# ⚠ 它讲的只是「在 Slack 里怎么用」,**不放命令表** —— 表是按群算的(权限/模型/画布都不同),
|
|
33
|
-
# 而 App Home 是按人发布的,放上去只能对一个群是准的。命令表仍走 `!help`。
|
|
34
|
-
app_home:
|
|
35
|
-
home_tab_enabled: true
|
|
36
|
-
messages_tab_enabled: true
|
|
37
|
-
messages_tab_read_only_enabled: false
|
|
38
|
-
# ⭐ 实测必需:不开这个,agents.sessions.setStatus 一律报 not_authorized。
|
|
39
|
-
# 它是 Agent Sessions(原生 Working… 状态 + Stop 按钮)的开关,与付不付费无关。
|
|
40
|
-
agent_view:
|
|
41
|
-
agent_description: 从群聊驱动无头编码 agent,跑完把结果回到线程里。
|
|
42
|
-
# ⚠⚠ **Slack 自己吃掉 `/` 开头的输入**(真机 2026-09-01:群里发 `/status`,消息根本没到过 bot)
|
|
43
|
-
# ⇒ 群内控制面(stop/skip/new/status/help/hud/model/issue)必须有别的入口。
|
|
44
|
-
# 这里注册**一个总入口**而不是 8 个命令:① Slack 内置就占着 /status、/remind 等常用名,
|
|
45
|
-
# 逐个注册会撞;② 一个入口只占一个名字,冲突面最小;③ 自带自动补全与 usage 提示。
|
|
46
|
-
# ⭐ 不重装 app 也有退路:适配器认替代前缀 `!stop`(见 docs/SLACK.md「群内控制面」)。
|
|
47
|
-
# ⚠⚠ **注册的斜杠命令在消息线程(thread)里根本调不出来**(官方明文,只有 Slack 内置命令与
|
|
48
|
-
# Giphy 例外)。而本产品的进度与回复都挂在线程里 ⇒ `/sint` 只在频道里有用,**主入口必须是
|
|
49
|
-
# `!` 前缀**;注册它的收益只剩「频道里有自动补全」。
|
|
50
|
-
# ⚠ 斜杠命令**不做 namespace**:同名后装的静默覆盖先装的,不报错也无从察觉 ⇒ `/sint` 随时可能
|
|
51
|
-
# 被客户 workspace 里别的 app 抢走。故前缀与总入口名都做成了配置项(slack.commandAlias /
|
|
52
|
-
# slack.umbrellaCommand)。
|
|
53
|
-
slash_commands:
|
|
54
|
-
- command: /sint
|
|
55
|
-
description: 控制这个群的任务
|
|
56
|
-
usage_hint: stop | skip | new | status | help | hud | model | issue
|
|
57
|
-
should_escape: false
|
|
58
|
-
|
|
59
|
-
oauth_config:
|
|
60
|
-
scopes:
|
|
61
|
-
bot:
|
|
62
|
-
# 收消息 —— ⚠ 四个前缀互不覆盖:channels=公开频道 / groups=私有频道 / im=单人DM / mpim=群组DM
|
|
63
|
-
- channels:history
|
|
64
|
-
- groups:history
|
|
65
|
-
- im:history
|
|
66
|
-
- mpim:history
|
|
67
|
-
# 频道信息与成员
|
|
68
|
-
- channels:read
|
|
69
|
-
- groups:read
|
|
70
|
-
- channels:join
|
|
71
|
-
- channels:manage
|
|
72
|
-
- groups:write
|
|
73
|
-
- users:read
|
|
74
|
-
# 发消息 · 编辑 · 删除(编辑删除只能对自己发的)
|
|
75
|
-
- chat:write
|
|
76
|
-
- chat:write.public
|
|
77
|
-
# 文件上下行
|
|
78
|
-
- files:read
|
|
79
|
-
- files:write
|
|
80
|
-
# reaction 回执
|
|
81
|
-
- reactions:write
|
|
82
|
-
# 主动发起 DM 会话才需要;收/回 DM 用 im:history + chat:write 就够
|
|
83
|
-
- im:write
|
|
84
|
-
# 上面注册了斜杠命令就必须有它,否则 Slack 拒收这份 manifest(「Slash Commands requires `commands` bot scope」)
|
|
85
|
-
- commands
|
|
86
|
-
|
|
87
|
-
settings:
|
|
88
|
-
event_subscriptions:
|
|
89
|
-
bot_events:
|
|
90
|
-
- message.channels
|
|
91
|
-
- message.groups
|
|
92
|
-
- message.im
|
|
93
|
-
- message.mpim
|
|
94
|
-
# ⚠ 不订阅它,setStatus 会返回 missing_agent_session_stopped_event_subscription 警告,
|
|
95
|
-
# 用户看到的是一个不可点击的转圈 —— 等于 Stop 按钮消失
|
|
96
|
-
- agent_session_stopped
|
|
97
|
-
# App Home 主页要靠它触发发布(⚠ 两个页签都会来,适配器按 tab==='home' 筛)
|
|
98
|
-
- app_home_opened
|
|
99
|
-
# ⚠ socket_mode_enabled 为 true 时不需要 request_url
|
|
100
|
-
interactivity:
|
|
101
|
-
is_enabled: true
|
|
102
|
-
org_deploy_enabled: false
|
|
103
|
-
socket_mode_enabled: true
|
|
104
|
-
token_rotation_enabled: false # ⚠ 这是单向开关,开了就关不掉,且 access token 会 12 小时过期
|
package/sbot.config.example.json
DELETED
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"agent": {
|
|
3
|
-
"bin": "claude",
|
|
4
|
-
"permissionMode": "dontAsk",
|
|
5
|
-
"maxTurnTimeMins": 30,
|
|
6
|
-
"maxConcurrent": 2,
|
|
7
|
-
"autocompactTokens": 400000,
|
|
8
|
-
"_perm_note": "'dontAsk' auto-denies any tool not in a group's allowedTools (and not in the CLI's read-only safe set), never hangs, enforced by Claude Code regardless of the backing model. TOOL PERMISSIONS ARE PER-PROJECT — set allowedTools/disallowedTools on each group below (not here). No global list; a group with none runs read-only (fail-closed).",
|
|
9
|
-
"_bin_note": "On Windows with an npm-global Claude Code, 'claude' is a claude.cmd shim — sbot resolves and runs it automatically (see docs/install.md). To pin an exact binary, set an absolute path, e.g. \"C:/Users/me/.local/bin/claude.exe\".",
|
|
10
|
-
"_autocompact_note": "上下文超过该 token 数即让 CLI 原地压缩会话(两个后端都生效;控制台「设置→运行限制」可改)。范围 100000–1000000,0 = 关闭(CLI 只在模型窗口边缘才压);区间外的值运行时会收编回边界。"
|
|
11
|
-
},
|
|
12
|
-
"timezone": "Asia/Shanghai",
|
|
13
|
-
"_timezone_note": "Product-wide time convention (IANA name). Message timestamps in prompts, the agent's TZ env, and the console clock all follow it — NOT the host system timezone. Default Asia/Shanghai (Beijing time); invalid values fall back to the default.",
|
|
14
|
-
"projectsRoots": [
|
|
15
|
-
"~/Projects"
|
|
16
|
-
],
|
|
17
|
-
"_projectsRoots_note": "项目根目录围栏(字符串或数组,认 ~):控制台里选目录 / 保存群绑定 / 从本机目录提炼风格,只认这些目录之内的路径(判据走 realpath,符号链接绕不出去)。不写 = ~/Projects(Windows 为 D:\\Projects,无 D 盘则 %USERPROFILE%\\Projects)。填 /、家目录、/Users 这类等于没围的值会被忽略并由 doctor 报出;配置文件里手写的越界老绑定照常运行,只在 doctor 里提示。",
|
|
18
|
-
"preview": {
|
|
19
|
-
"idleMin": 30,
|
|
20
|
-
"maxHours": 8,
|
|
21
|
-
"portFrom": 4610,
|
|
22
|
-
"portTo": 4629,
|
|
23
|
-
"host": "0.0.0.0"
|
|
24
|
-
},
|
|
25
|
-
"_preview_note": "Group preview service (`sbot preview`): the sanctioned way for a group's agent to put an interactive HTML prototype on the LAN. PER-GROUP OVERRIDE: `groups[<chatId>].preview` overrides any of these keys for that group — the console group card exposes the common case as a 「常驻(不自动停)」 checkbox (writes idleMin:0 + maxHours:0). Only the operator sets policy: `--config` is refused inside a group so the agent cannot swap it (otherwise an agent asked to keep a link open forever could write its own config file and turn every reclaim off). Runs as a detached worker (survives the turn — so the agent never has to stall a turn to keep a link alive) and is reaped by the daemon when idle. idleMin/maxHours: 0 disables that limit. Ports are assigned deterministically per group inside portFrom..portTo — open that range on the firewall once. host 0.0.0.0 is what makes it reachable for group members; set 127.0.0.1 to keep previews local-only (the agent is then told the link is NOT reachable from the LAN). HTTP is plaintext — trusted networks only. Serving the project root is refused unless explicitly requested (and announced in the group).",
|
|
26
|
-
"defaultProvider": "claude",
|
|
27
|
-
"_providers_note": "Model brains. Swap = ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN / ANTHROPIC_MODEL per spawn. Per-provider tuning (fast/slow split, 1M compaction, timeouts, provider quirks) goes in `env`, applied on top of the process env. Endpoints/model-ids for glm/deepseek/minimax/volcano are a verified set; gpt (needs a local translation router — see docs/GPT-ROUTER.md) and kimi (hidden, borrowed preset) are NOT yet verified end to end — confirm at setup. toolQuality is a hint shown in the console — verify a model on your own tasks before routing complex code work off Claude.",
|
|
28
|
-
"_vision_note": "Set \"vision\": false on any provider whose model is TEXT-ONLY (no image input). sbot then routes that provider's `claude` through a local strip-proxy that downgrades every image content block (screenshots, Read of a .png, MCP image returns) to a text placeholder — so image inputs no longer 400 the whole turn ('Model only support text input'). Omit or set true for vision-capable models (default = passthrough, unchanged). Auth/streaming are untouched; the proxy holds no secrets and listens on localhost only. Don't guess a model's capability — set false only when you've confirmed it rejects images.",
|
|
29
|
-
"providers": {
|
|
30
|
-
"claude": {
|
|
31
|
-
"toolQuality": "high",
|
|
32
|
-
"_note": "No baseUrl/apiKey -> uses your logged-in Claude Code auth (claude auth login). No \"model\" -> follows the CLI default model; set one in the console to pin a specific model. fallback: provider to switch to automatically when this one's subscription quota is used up (95% of a window, or a turn fails with a limit / account error); switches back when the window resets. Omit to disable.",
|
|
33
|
-
"fallback": "codex"
|
|
34
|
-
},
|
|
35
|
-
"glm": {
|
|
36
|
-
"baseUrl": "https://open.bigmodel.cn/api/anthropic",
|
|
37
|
-
"apiKey": "env:ZHIPU_API_KEY",
|
|
38
|
-
"model": "glm-5.2[1m]",
|
|
39
|
-
"toolQuality": "high",
|
|
40
|
-
"vision": false,
|
|
41
|
-
"env": {
|
|
42
|
-
"ANTHROPIC_DEFAULT_OPUS_MODEL": "glm-5.2[1m]",
|
|
43
|
-
"ANTHROPIC_DEFAULT_SONNET_MODEL": "glm-5.2[1m]",
|
|
44
|
-
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "glm-4.5-air",
|
|
45
|
-
"CLAUDE_CODE_SUBAGENT_MODEL": "glm-4.5-air",
|
|
46
|
-
"CLAUDE_CODE_AUTO_COMPACT_WINDOW": "1000000",
|
|
47
|
-
"API_TIMEOUT_MS": "3000000",
|
|
48
|
-
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
|
|
49
|
-
"DISABLE_TELEMETRY": "1",
|
|
50
|
-
"DISABLE_ERROR_REPORTING": "1"
|
|
51
|
-
},
|
|
52
|
-
"_note": "Zhipu GLM Anthropic-compatible endpoint. [1m] + AUTO_COMPACT_WINDOW enable 1M context; fast/slow split routes sub-agents to glm-4.5-air. GLM-5.2 is TEXT-ONLY -> vision:false engages the strip-proxy (without it, a browser screenshot fails the whole turn with a 400). Docs: docs.bigmodel.cn/cn/coding-plan/tool/claude"
|
|
53
|
-
},
|
|
54
|
-
"deepseek": {
|
|
55
|
-
"baseUrl": "https://api.deepseek.com/anthropic",
|
|
56
|
-
"apiKey": "env:DEEPSEEK_API_KEY",
|
|
57
|
-
"model": "deepseek-v4-pro[1m]",
|
|
58
|
-
"toolQuality": "medium",
|
|
59
|
-
"env": {
|
|
60
|
-
"ANTHROPIC_DEFAULT_OPUS_MODEL": "deepseek-v4-pro[1m]",
|
|
61
|
-
"ANTHROPIC_DEFAULT_SONNET_MODEL": "deepseek-v4-pro[1m]",
|
|
62
|
-
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "deepseek-v4-flash",
|
|
63
|
-
"CLAUDE_CODE_SUBAGENT_MODEL": "deepseek-v4-flash",
|
|
64
|
-
"CLAUDE_CODE_ATTRIBUTION_HEADER": "0",
|
|
65
|
-
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
|
|
66
|
-
"DISABLE_TELEMETRY": "1",
|
|
67
|
-
"DISABLE_ERROR_REPORTING": "1"
|
|
68
|
-
},
|
|
69
|
-
"_note": "DeepSeek Anthropic-compatible endpoint. ATTRIBUTION_HEADER=0 keeps the DeepSeek prefix cache from busting each turn (claude-code issue #40652); sub-agents run on the cheaper v4-flash. Docs: api-docs.deepseek.com/.../claude_code"
|
|
70
|
-
},
|
|
71
|
-
"minimax": {
|
|
72
|
-
"baseUrl": "https://api.minimaxi.com/anthropic",
|
|
73
|
-
"apiKey": "env:MINIMAX_API_KEY",
|
|
74
|
-
"model": "MiniMax-M3[1m]",
|
|
75
|
-
"toolQuality": "medium",
|
|
76
|
-
"env": {
|
|
77
|
-
"ANTHROPIC_DEFAULT_OPUS_MODEL": "MiniMax-M3[1m]",
|
|
78
|
-
"ANTHROPIC_DEFAULT_SONNET_MODEL": "MiniMax-M3[1m]",
|
|
79
|
-
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "MiniMax-M3[1m]",
|
|
80
|
-
"API_TIMEOUT_MS": "3000000",
|
|
81
|
-
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
|
|
82
|
-
"DISABLE_TELEMETRY": "1",
|
|
83
|
-
"DISABLE_ERROR_REPORTING": "1"
|
|
84
|
-
},
|
|
85
|
-
"_note": "MiniMax M3 Anthropic-compatible endpoint. Single model across all tiers (no fast/slow split). Strong at web-browsing/search agents. Docs: platform.minimaxi.com/docs/token-plan/claude-code"
|
|
86
|
-
},
|
|
87
|
-
"volcano": {
|
|
88
|
-
"baseUrl": "https://ark.cn-beijing.volces.com/api/coding",
|
|
89
|
-
"apiKey": "env:ARK_API_KEY",
|
|
90
|
-
"model": "glm-5.2[1m]",
|
|
91
|
-
"toolQuality": "high",
|
|
92
|
-
"vision": false,
|
|
93
|
-
"env": {
|
|
94
|
-
"ANTHROPIC_DEFAULT_OPUS_MODEL": "glm-5.2[1m]",
|
|
95
|
-
"ANTHROPIC_DEFAULT_SONNET_MODEL": "glm-5.2[1m]",
|
|
96
|
-
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "deepseek-v4-pro",
|
|
97
|
-
"CLAUDE_CODE_SUBAGENT_MODEL": "glm-5.2[1m]",
|
|
98
|
-
"CLAUDE_CODE_AUTO_COMPACT_WINDOW": "1000000",
|
|
99
|
-
"API_TIMEOUT_MS": "3000000",
|
|
100
|
-
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
|
|
101
|
-
"DISABLE_TELEMETRY": "1",
|
|
102
|
-
"DISABLE_ERROR_REPORTING": "1"
|
|
103
|
-
},
|
|
104
|
-
"_note": "Volcano Ark Coding Plan — one endpoint packages GLM-5.2 / DeepSeek / etc.; switch by changing `model`. Console shows a percent bar only (no token API). vision:false matches the GLM-5.2 model above — if you switch `model` to a multimodal one, drop it. Docs: volcengine.com/docs/82379/1928262"
|
|
105
|
-
},
|
|
106
|
-
"gpt": {
|
|
107
|
-
"baseUrl": "http://127.0.0.1:3456",
|
|
108
|
-
"apiKey": "sk-local-dummy",
|
|
109
|
-
"model": "gpt-5.5",
|
|
110
|
-
"toolQuality": "medium",
|
|
111
|
-
"hidden": true,
|
|
112
|
-
"_note": "[OPTIONAL — platform-API-key path. The default GPT route is the `codex` provider below (Codex CLI: spawns `codex exec`, auth via `codex login`; docs/CODEX-RUNNER.md). Kept hidden as a template for users who DO have an OpenAI platform key (subscription-driven Codex is rate-limited and multi-user use raises subscription-terms questions, so a platform key is the more reliable path).] GPT/OpenAI via a LOCAL translation router (claude-code-router / LiteLLM). OpenAI has NO Anthropic endpoint, so `claude` can't hit it directly like the /anthropic providers above — baseUrl points at YOUR running router (CCR default port shown; confirm yours), which translates Anthropic Messages <-> OpenAI Responses. KEYS: the ONLY real credential is your OpenAI key, and it lives in the ROUTER's config, never here. apiKey above is a DUMMY — without a token `claude` falls back to your logged-in OAuth credential, which a custom endpoint rejects, so the dummy just overrides that; any non-empty string works because a loopback router isn't auth-protected by default. ONLY if you deliberately set the router's own APIKEY must apiKey equal it (then use \"env:CCR_API_KEY\"). `sbot doctor` probes this loopback address and reports the `router:gpt` row as unreachable if the router isn't up, so the provider fails fast, never hangs. Prefer a Responses-API route (keeps cross-tool reasoning continuity). toolQuality:medium so hard code tasks still route to Claude. If `claude` rejects a non-Claude model id, set a Claude-safe alias here and map it router-side. If the router mishandles images, add \"vision\": false. Route decision: DESIGN section 6.1."
|
|
113
|
-
},
|
|
114
|
-
"kimi": {
|
|
115
|
-
"baseUrl": "https://api.moonshot.cn/anthropic",
|
|
116
|
-
"apiKey": "env:MOONSHOT_API_KEY",
|
|
117
|
-
"model": "kimi-k2.7-code",
|
|
118
|
-
"toolQuality": "medium",
|
|
119
|
-
"hidden": true,
|
|
120
|
-
"_note": "Kimi / Moonshot Anthropic-compatible endpoint (native — no router needed, works like glm/deepseek above). Borrowed from a maintained provider-preset table; VERIFY base_url + model id at setup (third-party model ids churn). hidden:true keeps it inert until you confirm and enable. Docs: platform.moonshot.cn"
|
|
121
|
-
},
|
|
122
|
-
"codex": {
|
|
123
|
-
"runner": "codex",
|
|
124
|
-
"toolQuality": "high",
|
|
125
|
-
"reasoningEffort": "medium",
|
|
126
|
-
"_note": "GPT via Codex CLI — this project's default GPT path. NO baseUrl/apiKey: sbot spawns `codex exec` and auth is the machine's own `codex login` (ChatGPT coding-plan SUBSCRIPTION), never a key in config. Set a group's provider to `codex` to run GPT there. Sandbox comes from the group's permissionMode: bypassPermissions -> danger-full-access (lets GPT write files; Windows has no workspace-write sandbox) — set it only for trusted private groups; default dontAsk -> read-only (fail-closed). GPT shares your interactive Codex subscription quota (5h/weekly) — use sparingly. runner:'codex' selects this backend; Claude's allow/deny + MCP gating do NOT apply. reasoningEffort (minimal/low/medium/high) → `-c model_reasoning_effort`; omit for the model default (minimal/low is faster + saves quota, high is stronger/slower). No `model` on purpose: codex then uses its own default (the `model` in ~/.codex/config.toml, else the official default). A pinned id goes stale as new models ship, and some ids are rejected for ChatGPT-account logins (\"model is not supported when using Codex with a ChatGPT account\") — check `codex debug models` before pinning one. Details: docs/CODEX-RUNNER.md."
|
|
127
|
-
}
|
|
128
|
-
},
|
|
129
|
-
"_alerts_note": "可选顶层 alerts 段,形如 {\"chatId\": \"oc_xxx\"}:运维告警群。模型账号异常(被停 / 登录态刷新失败)、订阅额度用完、群自动切换备用模型 / 切回,都会发一条到这个群 —— 不配的话,这些只在出事的那个群里说一句,你不在那个群就不知道。告警群要**保持未绑定项目**(绑了助手就会在里面干活,doctor 会 warn);去重:同一模型同类故障每小时最多一条,「已恢复」只在报过故障之后才报。不配 = 不发(doctor 只提示一句)。",
|
|
130
|
-
"_browser_note": "可选顶层 browser 段(docs/BROWSER.md),形如 {\"cdpUrl\": \"http://127.0.0.1:9222\", \"takeoverMinutes\": 10}:cdpUrl = 常驻 Chrome 的远程调试地址(默认本机 9222);takeoverMinutes = 群里发的「接管浏览器」一次性链接有效期(分钟,默认 10,范围 1~60)。这里故意不写 browser 键:不配 = 用默认值,而且 doctor 探不到浏览器也不告警(没有常驻 Chrome 的机器照抄示例不会平白多一条警告);配了才把探不到当 warn。",
|
|
131
|
-
"_proxy_note": "可选代理分流:官方 claude 走代理、国内端点直连。填了 proxy.http 才生效;sbot 会注入子进程 env(不覆盖 shell 已有值),daemon 自己出网的部分(Slack 的 REST 与 WebSocket)也走同一份配置与 noProxy(飞书 SDK 自带网络栈,不经此处)。`sbot setup` 能按你启用的 provider 自动生成 noProxy。留空/删除本块=不用代理。",
|
|
132
|
-
"proxy": {
|
|
133
|
-
"http": "",
|
|
134
|
-
"https": "",
|
|
135
|
-
"noProxy": ""
|
|
136
|
-
},
|
|
137
|
-
"_platform_note": "聊天平台可以同时接:配了 feishu 块(appId/appSecret)就接飞书,配了 slack 块(botToken/appToken)就接 Slack,配了 wecom 块(botId/secret)就接企业微信;每个群用 groups.<id>.platform(\"feishu\" | \"slack\" | \"wecom\")说明属于哪边,没写的按群 id 形态推断(oc_ 飞书 / C·G·D 开头的大写 id 归 Slack / wr 企微;企微单聊 id 没形态,只配了一个平台时归它)。各平台开通见 docs/platforms/。",
|
|
138
|
-
"_slack_note": "Slack:Socket Mode 长连接,免公网。用 docs/slack-app-manifest.yaml 建 App,取 Bot Token(xoxb-)与 App-Level Token(xapp-,connections:write);onlyWhenMentioned:true = 频道里只回 @ 机器人的消息;群内命令用 ! 前缀(Slack 会吃掉 / 开头的输入)。要启用就把 _slack_example 改名为 slack(docs/platforms/slack.md)",
|
|
139
|
-
"_slack_example": {
|
|
140
|
-
"botToken": "env:SLACK_BOT_TOKEN",
|
|
141
|
-
"appToken": "env:SLACK_APP_TOKEN",
|
|
142
|
-
"onlyWhenMentioned": true,
|
|
143
|
-
"replyInThread": true
|
|
144
|
-
},
|
|
145
|
-
"_wecom_note": "企业微信:后台「智能机器人 → 新建 → API 模式 → 使用长连接」拿 Bot ID + Secret;botName 用来剥正文里的 @;names 手工映射 userid→显示名(没装官方命令行工具时群里只显示 userid);welcome 首次单聊的欢迎语(可省);pollSec / followSec / keywords = 「不 @ 也回复」(轮询群历史的平时间隔〔刚聊过 8 秒一看、久没人说话 2 分钟一看,@ 的消息不走轮询随时即答〕/ 跟进窗口 / 点名词);gatekeeper = 它的第二道门,规则不放行的消息先问这个便宜模型要不要答(只能指有 baseUrl+apiKey 的模型;可省 = 没有这道门);要启用就把 _wecom_example 改名为 wecom(docs/WECOM.md)",
|
|
146
|
-
"_wecom_example": {
|
|
147
|
-
"botId": "aibXXXXXXXX",
|
|
148
|
-
"secret": "env:WECOM_BOT_SECRET",
|
|
149
|
-
"botName": "SintBot",
|
|
150
|
-
"names": {},
|
|
151
|
-
"welcome": "",
|
|
152
|
-
"cli": "wecom-cli",
|
|
153
|
-
"pollSec": 0,
|
|
154
|
-
"followSec": 300,
|
|
155
|
-
"keywords": [
|
|
156
|
-
"机器人",
|
|
157
|
-
"助手"
|
|
158
|
-
],
|
|
159
|
-
"gatekeeper": {
|
|
160
|
-
"provider": "deepseek",
|
|
161
|
-
"model": "deepseek-v4-flash"
|
|
162
|
-
}
|
|
163
|
-
},
|
|
164
|
-
"feishu": {
|
|
165
|
-
"appId": "env:FEISHU_APP_ID",
|
|
166
|
-
"appSecret": "env:FEISHU_APP_SECRET",
|
|
167
|
-
"botOpenId": "ou_xxxxxxxxxxxxxxxx",
|
|
168
|
-
"onlyWhenMentioned": false,
|
|
169
|
-
"ws": {
|
|
170
|
-
"pingTimeoutSec": 30,
|
|
171
|
-
"handshakeTimeoutMs": 15000,
|
|
172
|
-
"tickIntervalMs": 30000,
|
|
173
|
-
"staleReconnectingMs": 120000,
|
|
174
|
-
"backoffBaseMs": 2000,
|
|
175
|
-
"backoffCapMs": 60000,
|
|
176
|
-
"_note": "WS 活性看门狗调优(全可选,不写=用这些默认)。pingTimeoutSec 最关键:ping 无应答这么久就判死→重连(治『机器人变聋』);其余为强制重建的兜底节奏。按实际网络情况再调。"
|
|
177
|
-
},
|
|
178
|
-
"card": {
|
|
179
|
-
"throttleMs": 1500,
|
|
180
|
-
"_note": "流式打字机卡片的推送节流窗(ms)。模型每说一段就刷一次卡片=一次计入飞书月调用量的 API 调用;累积文本丢中间帧无损,故窗内只保留最后一帧(窗到补发)。调大=更省额度、打字机更粗(一行一行地出);0=逐帧推(最顺滑最费)。首帧不等待。"
|
|
181
|
-
},
|
|
182
|
-
"board": {
|
|
183
|
-
"pollSec": 0,
|
|
184
|
-
"holdRounds": 5,
|
|
185
|
-
"_note": "设计画布自动感知(docs/BOARD.md),默认关:pollSec=0 时画布标注不自动播报,群成员回群里说一声 agent 就现拉。要开把 pollSec 设成 120(最小 30)= 对建过画布的群每隔这么多秒拉一次画板节点+评论,每群每轮约 2 次飞书 API 调用,120 秒一轮约每群每月 4 万次,计入飞书月调用量。holdRounds=用户还在画时最多押后几轮再播。改了要重启 daemon。"
|
|
186
|
-
},
|
|
187
|
-
"_note": "For `sbot start`. Default handles all group messages EXCEPT those @mentioning others-but-not-the-bot (needs botOpenId to tell them apart). Set onlyWhenMentioned:true to respond only when the bot is @'d. appId/appSecret support 'env:VAR'."
|
|
188
|
-
},
|
|
189
|
-
"groups": {
|
|
190
|
-
"oc_example_chat_id": {
|
|
191
|
-
"workdir": "/abs/path/to/project-a",
|
|
192
|
-
"provider": "claude",
|
|
193
|
-
"allowedTools": [
|
|
194
|
-
"Read",
|
|
195
|
-
"Grep",
|
|
196
|
-
"Glob",
|
|
197
|
-
"Edit",
|
|
198
|
-
"Write",
|
|
199
|
-
"TodoWrite",
|
|
200
|
-
"WebSearch",
|
|
201
|
-
"WebFetch",
|
|
202
|
-
"Bash(npm *)",
|
|
203
|
-
"Bash(pnpm *)",
|
|
204
|
-
"Bash(yarn *)",
|
|
205
|
-
"Bash(npx *)",
|
|
206
|
-
"Bash(node *)",
|
|
207
|
-
"Bash(python *)",
|
|
208
|
-
"Bash(python3 *)",
|
|
209
|
-
"Bash(pip *)",
|
|
210
|
-
"Bash(pytest *)",
|
|
211
|
-
"Bash(git status)",
|
|
212
|
-
"Bash(git diff *)",
|
|
213
|
-
"Bash(git log *)",
|
|
214
|
-
"Bash(git add *)",
|
|
215
|
-
"Bash(git commit *)",
|
|
216
|
-
"Bash(git checkout *)",
|
|
217
|
-
"Bash(git branch *)",
|
|
218
|
-
"Bash(git stash *)",
|
|
219
|
-
"Bash(ls *)",
|
|
220
|
-
"Bash(mkdir *)"
|
|
221
|
-
],
|
|
222
|
-
"disallowedTools": [
|
|
223
|
-
"Bash(sudo *)",
|
|
224
|
-
"Bash(rm -rf *)",
|
|
225
|
-
"Bash(git push *)",
|
|
226
|
-
"Bash(curl *)",
|
|
227
|
-
"Bash(wget *)",
|
|
228
|
-
"Bash(ssh *)",
|
|
229
|
-
"Bash(scp *)"
|
|
230
|
-
],
|
|
231
|
-
"_note": "one project per group: chatId -> { workdir, provider?, model?, allowedTools?, disallowedTools? }. Omit provider to use defaultProvider. Permissions are PER-PROJECT and scoped to this workdir (cwd=workdir, no external --add-dir); tune allow/deny per project. Omit both ⇒ read-only (fail-closed). The lists above = the console's 开发 preset (只读=omit both, 全信任=Bash(*) etc with a strong warning). On Windows use a real path, e.g. \"C:/Users/me/project-a\"."
|
|
232
|
-
}
|
|
233
|
-
},
|
|
234
|
-
"_secrets": "apiKey/appId/appSecret/botOpenId/botToken/appToken/secret support 'env:VAR_NAME' to read from the environment instead of hardcoding. `sbot setup` seeds ~/.sbot/config.json from this file; or copy it yourself and fill in. Field reference: docs/config.md."
|
|
235
|
-
}
|
package/src/agent/askonce.js
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
// 问一次模型、拿一段文字:不起 CLI、不带会话、不开工具 —— 直接打 provider 的 HTTP 端点。
|
|
2
|
-
//
|
|
3
|
-
// 给谁用:企业微信「不 @ 也回复」的便宜模型门控(src/platform/wecomgate.js)这类「每条消息问一句 YES / NO」的活。
|
|
4
|
-
// 起一个 claude / codex 子进程要 2~5 秒、系统提示本身就几千 token,拿来判一句话是杀鸡用牛刀;而第三方 provider
|
|
5
|
-
// 在配置里本来就有 baseUrl + apiKey(消息协议与 claude CLI 用的是同一套),一个 POST 就够。
|
|
6
|
-
//
|
|
7
|
-
// 只支持「有 baseUrl + apiKey」的 provider:订阅登录的 claude(没有 key)与 codex(不是这套协议)不行 —— 调用方
|
|
8
|
-
// (doctor / 接线层)按 askableProvider 先判,不行就说清为什么,不在这里猜。
|
|
9
|
-
// 请求形状 = 消息协议最小集:{ model, max_tokens, system, messages:[{role:'user',content}] };鉴权两个头都带
|
|
10
|
-
// (Bearer 是 claude CLI 对这些端点用的方式,x-api-key 是协议原生写法,各家只认其一的都见过)。
|
|
11
|
-
// model 名里 claude CLI 专用的 `[1m]` 后缀(1M 上下文标记)要剥掉 —— 端点不认。
|
|
12
|
-
// ⚠ Node 的 fetch 不读 HTTP(S)_PROXY 环境变量:走代理才通的端点这里连不上(需要代理才能出网的机器)。门控失败只记日志、当 NO,不影响主流程。
|
|
13
|
-
|
|
14
|
-
const DEFAULT_TIMEOUT_MS = 8_000
|
|
15
|
-
const DEFAULT_MAX_TOKENS = 48
|
|
16
|
-
|
|
17
|
-
// provider 能不能这样问:返回 null = 能;否则一句人话说明为什么不能(doctor / 日志共用同一口径)
|
|
18
|
-
// opts.model = 调用方按次指定的型号;provider 自己没写 model 且这里也没给 ⇒ 请求必失败(端点要型号名),doctor 得提前说
|
|
19
|
-
function askableProvider(p, name = '', { model } = {}) {
|
|
20
|
-
if (!p || typeof p !== 'object') return `没有叫「${name}」的模型配置`
|
|
21
|
-
if (p.runner === 'codex') return `「${name}」走的不是这套消息协议,不能当门控`
|
|
22
|
-
if (!p.baseUrl || !p.apiKey) return `「${name}」没有配 baseUrl + apiKey(订阅登录的模型没法直接 HTTP 调用),不能当门控`
|
|
23
|
-
if (!stripModelTag(model || p.model)) return `「${name}」没有型号名(模型配置里没写 model,门控也没指定),不知道该问哪个型号`
|
|
24
|
-
return null
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const stripModelTag = (m) => String(m || '').replace(/\[1m\]$/i, '').trim()
|
|
28
|
-
|
|
29
|
-
function createAskOnce({ fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
30
|
-
if (typeof fetchImpl !== 'function') throw new Error('askonce: fetch unavailable')
|
|
31
|
-
/**
|
|
32
|
-
* @param {{baseUrl:string, apiKey:string, model?:string}} provider
|
|
33
|
-
* @param {{system?:string, user:string, model?:string, maxTokens?:number}} q
|
|
34
|
-
* @returns {Promise<string>} 模型回的文字(多段只拼 text 段);非 2xx / 超时 / 形状不对都 throw
|
|
35
|
-
*/
|
|
36
|
-
return async function askOnce(provider, q) {
|
|
37
|
-
const why = askableProvider(provider, provider && provider.name, { model: q.model })
|
|
38
|
-
if (why) throw new Error(why)
|
|
39
|
-
const model = stripModelTag(q.model || provider.model)
|
|
40
|
-
const url = String(provider.baseUrl).replace(/\/+$/, '') + '/v1/messages'
|
|
41
|
-
// max_tokens 默认 48 不是 8:思考型型号会先吐 thinking 段,预算太小就一个字都轮不到 text 段,门控永远「看不懂」
|
|
42
|
-
const body = { model, max_tokens: Math.max(1, q.maxTokens || DEFAULT_MAX_TOKENS), messages: [{ role: 'user', content: String(q.user || '') }] }
|
|
43
|
-
if (q.system) body.system = String(q.system)
|
|
44
|
-
// 默认关思考:实测(DeepSeek v4 的消息协议端点)不传就先吐 thinking 段把 48 个 token 全吃光、一个字答不出;传
|
|
45
|
-
// `thinking:{type:'disabled'}` 后 1 个输出 token 0.7 秒答 YES。端点不认这个字段会 400 并在原话里提到 thinking ⇒ 退一次不带它重试。
|
|
46
|
-
const post = async (b) => {
|
|
47
|
-
const ac = new AbortController()
|
|
48
|
-
const t = setTimeout(() => ac.abort(), timeoutMs)
|
|
49
|
-
try {
|
|
50
|
-
// ⚠ 超时必须罩住 r.text():响应头到了、body 停滞(网关半开 / 限流挂起)是常见形态,只罩 fetch 本身会让调用方永久挂起 ——
|
|
51
|
-
// 而调用方(轮询 tick)是串行 await,一次挂起 = 所有企微群的「不 @ 也回复」静默停摆(对抗审查逮到)
|
|
52
|
-
const r = await fetchImpl(url, {
|
|
53
|
-
method: 'POST',
|
|
54
|
-
headers: { 'content-type': 'application/json', 'anthropic-version': '2023-06-01', 'x-api-key': provider.apiKey, authorization: `Bearer ${provider.apiKey}` },
|
|
55
|
-
body: JSON.stringify(b),
|
|
56
|
-
signal: ac.signal,
|
|
57
|
-
})
|
|
58
|
-
return { status: r.status, ok: r.ok, txt: await r.text() }
|
|
59
|
-
} catch (e) {
|
|
60
|
-
throw new Error(e && e.name === 'AbortError' ? `超时(${timeoutMs}ms)` : `请求失败:${(e && e.message) || e}`)
|
|
61
|
-
} finally { clearTimeout(t) }
|
|
62
|
-
}
|
|
63
|
-
let { status, ok, txt } = await post(q.thinking === false ? body : { ...body, thinking: { type: 'disabled' } })
|
|
64
|
-
if (!ok && status === 400 && q.thinking !== false && /thinking/i.test(String(txt))) ({ status, ok, txt } = await post(body))
|
|
65
|
-
if (!ok) throw new Error(`HTTP ${status}:${String(txt).slice(0, 160)}`)
|
|
66
|
-
let j
|
|
67
|
-
try { j = JSON.parse(txt) } catch { throw new Error('回包不是 JSON:' + String(txt).slice(0, 80)) }
|
|
68
|
-
const parts = Array.isArray(j && j.content) ? j.content : []
|
|
69
|
-
const out = parts.filter((c) => c && c.type === 'text').map((c) => c.text).join('')
|
|
70
|
-
if (!out && !parts.length) throw new Error('回包里没有内容')
|
|
71
|
-
if (!out) throw new Error(`回包只有 ${parts.map((c) => c && c.type).join('/')} 段没有文字(多半是思考型型号把 token 预算吃光了,换非思考型号)`)
|
|
72
|
-
return out
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
module.exports = { createAskOnce, askableProvider, stripModelTag, DEFAULT_TIMEOUT_MS, DEFAULT_MAX_TOKENS }
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
// Claude 模型清单(控制台「模型」下拉用)—— **只在已登录时给**,未登录不列任何型号。
|
|
2
|
-
//
|
|
3
|
-
// ⚠ 为什么不是「登录后拉官方实时清单」:查过了,三条路都不通(2026-08-03 实测):
|
|
4
|
-
// 1. `GET /v1/models` + Claude Code 的登录态 token(Bearer + anthropic-beta: oauth-2025-04-20)
|
|
5
|
-
// —— curl 拿得到 200 与完整清单,但**同样的头从 Node 里发一律 403 `Request not allowed`**
|
|
6
|
-
// (node:https / fetch 都试过,补 accept、换 UA、换 HTTP/1.1 全无效)⇒ 这是按客户端指纹的
|
|
7
|
-
// 门禁,不是缺哪个头。它拦的是「登录态 token 被别的程序拿去用」,shell 出 curl 去绕正是
|
|
8
|
-
// 它要防的那件事,所以不做。
|
|
9
|
-
// 2. `claude` 没有 models 子命令(只有 auth / mcp / plugin / doctor …)。
|
|
10
|
-
// 3. `~/.claude.json` 的 `modelAccessCache` 实测是空数组、`orgModelDefaultCache` 是 null
|
|
11
|
-
// —— 不是清单来源。
|
|
12
|
-
// 所以清单 = 内置候选 + 本机账号的**额外可选项**(`additionalModelOptionsCache`,CLI 登录后
|
|
13
|
-
// 自己写的,合法本地读、best-effort),并在面板里说清「以 Claude Code 里 /model 为准」。
|
|
14
|
-
// 想用清单外的型号:面板留了「自定义…」直接填,不受这里限制。
|
|
15
|
-
|
|
16
|
-
const fs = require('node:fs')
|
|
17
|
-
const os = require('node:os')
|
|
18
|
-
const path = require('node:path')
|
|
19
|
-
|
|
20
|
-
// 当前主力档 + 上一代 Opus。刻意不铺满历史版本 —— 它是挑选快捷方式,不是权威清单;
|
|
21
|
-
// 4.8 留着是因为它仍在服务且额度池与 Opus 5 分开(operator 点名要能选,2026-08-05)。
|
|
22
|
-
// ⚠ id 是不带日期后缀的固定串(`claude-opus-4-8`,不是 `claude-opus-4-8-2026xxxx`)。
|
|
23
|
-
const BUILTIN = [
|
|
24
|
-
{ id: '', name: '默认(跟随 Claude Code)', note: '推荐 · 不指定型号' },
|
|
25
|
-
{ id: 'claude-opus-5', name: 'Claude Opus 5', note: '最强 · 复杂改码 / 长链路' },
|
|
26
|
-
{ id: 'claude-opus-4-8', name: 'Claude Opus 4.8', note: '上一代 Opus · 额度池独立' },
|
|
27
|
-
{ id: 'claude-sonnet-5', name: 'Claude Sonnet 5', note: '速度与智能平衡' },
|
|
28
|
-
{ id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5', note: '最快最省 · 简单任务' },
|
|
29
|
-
]
|
|
30
|
-
|
|
31
|
-
// CLI 登录后写在 ~/.claude.json 的账号额外可选模型(本机这台是 Fable 5)。
|
|
32
|
-
// 私有字段,形状可能变 ⇒ 全程防御式读取,取不到就当没有。纯函数,便于单测。
|
|
33
|
-
function parseExtraModels(rawJson) {
|
|
34
|
-
const arr = rawJson && Array.isArray(rawJson.additionalModelOptionsCache) ? rawJson.additionalModelOptionsCache : []
|
|
35
|
-
return arr
|
|
36
|
-
.filter((x) => x && typeof x.value === 'string' && x.value)
|
|
37
|
-
.map((x) => ({ id: x.value, name: x.label || x.value, note: String(x.description || '').slice(0, 60) }))
|
|
38
|
-
}
|
|
39
|
-
function readExtraModels(home) {
|
|
40
|
-
try { return parseExtraModels(JSON.parse(fs.readFileSync(path.join(home || os.homedir(), '.claude.json'), 'utf8'))) }
|
|
41
|
-
catch { return [] }
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// 合并:内置在前,账号额外项去重后追加(同 id 不重复列)。
|
|
45
|
-
function mergeModels(extra = []) {
|
|
46
|
-
const out = BUILTIN.slice()
|
|
47
|
-
const seen = new Set(out.map((m) => m.id))
|
|
48
|
-
for (const m of extra) if (m.id && !seen.has(m.id)) { seen.add(m.id); out.push({ ...m, extra: true }) }
|
|
49
|
-
return out
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// { loggedIn, models, source }。未登录 ⇒ models 为空数组(面板据此整块不显示型号)。
|
|
53
|
-
// source: 'account' = 含本机账号的额外可选项 · 'builtin' = 只有内置候选。
|
|
54
|
-
function listModels({ loggedIn, home } = {}) {
|
|
55
|
-
if (!loggedIn) return { loggedIn: false, models: [], source: 'none' }
|
|
56
|
-
const extra = readExtraModels(home)
|
|
57
|
-
return { loggedIn: true, models: mergeModels(extra), source: extra.length ? 'account' : 'builtin' }
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
module.exports = { listModels, mergeModels, parseExtraModels, readExtraModels, BUILTIN }
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
// Claude 订阅登录态探测 —— 控制台 Claude 模型卡 + doctor 用。
|
|
2
|
-
//
|
|
3
|
-
// `claude auth status --json` 是**官方的非交互子命令**(0.2s、不联网收费、不吃订阅额度),
|
|
4
|
-
// 返回 {loggedIn, authMethod, apiProvider, email, orgId, orgName, subscriptionType}。
|
|
5
|
-
// 它是 Claude 这侧对应 `codex login status` 的那件东西 —— 在它之前,sbot 只能按
|
|
6
|
-
// 「没填 baseUrl 就当成登录态」猜(卡片写「登录态·未验证」、doctor 写「Claude 登录态(未验证)」),
|
|
7
|
-
// 真没登录时一样显示正常,要到群里跑一轮才炸。
|
|
8
|
-
//
|
|
9
|
-
// 一律 best-effort:失败不抛,调用方显示「未验证」而不是把整块健康检查拖垮。
|
|
10
|
-
|
|
11
|
-
const { spawn } = require('node:child_process')
|
|
12
|
-
const { resolveSpawn } = require('./winexec')
|
|
13
|
-
|
|
14
|
-
// `claude auth status --json` 的 stdout → 归一化结果。纯函数,便于单测钉住字段映射。
|
|
15
|
-
// 老版本 CLI 没有这个子命令(stderr 报 unknown command / 非 0 退出)⇒ ok:false,
|
|
16
|
-
// 调用方据此显示「未验证」,**不能**当成「未登录」—— 二者要分开,同飞书应用描述那条教训。
|
|
17
|
-
function parseAuthStatus(stdout, code) {
|
|
18
|
-
const text = String(stdout || '').trim()
|
|
19
|
-
let j = null
|
|
20
|
-
try { j = JSON.parse(text) } catch { /* 非 JSON:老版本或报错文本 */ }
|
|
21
|
-
if (!j || typeof j !== 'object') {
|
|
22
|
-
return { ok: false, error: text.slice(0, 200) || `exit ${code}`, loggedIn: false }
|
|
23
|
-
}
|
|
24
|
-
return {
|
|
25
|
-
ok: true,
|
|
26
|
-
loggedIn: !!j.loggedIn,
|
|
27
|
-
plan: j.subscriptionType || '', // max / pro / team …(未订阅时为空)
|
|
28
|
-
email: j.email || '',
|
|
29
|
-
org: j.orgName || '',
|
|
30
|
-
method: j.authMethod || '', // claude.ai = 订阅登录
|
|
31
|
-
apiProvider: j.apiProvider || '', // firstParty / bedrock / vertex
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function loginStatus(bin = 'claude', timeoutMs = 8000) {
|
|
36
|
-
return new Promise((resolve) => {
|
|
37
|
-
const { file, spawnArgs } = resolveSpawn(bin, ['auth', 'status', '--json'])
|
|
38
|
-
let out = '', err = '', settled = false
|
|
39
|
-
const done = (v) => { if (settled) return; settled = true; clearTimeout(timer); resolve(v) }
|
|
40
|
-
let child
|
|
41
|
-
try { child = spawn(file, spawnArgs, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true }) }
|
|
42
|
-
catch (e) { return resolve({ ok: false, error: e.message, loggedIn: false }) }
|
|
43
|
-
const timer = setTimeout(() => { try { child.kill() } catch {} done({ ok: false, error: 'timeout', loggedIn: false }) }, timeoutMs)
|
|
44
|
-
child.stdout.on('data', (c) => { out += c.toString('utf8') })
|
|
45
|
-
child.stderr.on('data', (c) => { err += c.toString('utf8') })
|
|
46
|
-
child.on('error', (e) => done({ ok: false, loggedIn: false, error: e.code === 'ENOENT' ? "'claude' 不在 PATH 上" : e.message }))
|
|
47
|
-
child.on('close', (code) => done(parseAuthStatus(out || err, code)))
|
|
48
|
-
})
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// TTL 缓存:doctor 是一次性的,但控制台 /api/state 每次动作 + 60s 自动刷新都会重建健康行,
|
|
52
|
-
// 每次都 spawn 一个进程没必要。登录态分钟级变化足够,取 60s。
|
|
53
|
-
const CACHE = new Map() // bin -> { at, val }
|
|
54
|
-
const TTL = 60_000
|
|
55
|
-
async function cachedLoginStatus(bin = 'claude', ttl = TTL) {
|
|
56
|
-
const c = CACHE.get(bin)
|
|
57
|
-
if (c && Date.now() - c.at < ttl) return c.val
|
|
58
|
-
const val = await loginStatus(bin)
|
|
59
|
-
CACHE.set(bin, { at: Date.now(), val })
|
|
60
|
-
return val
|
|
61
|
-
}
|
|
62
|
-
function clearCache() { CACHE.clear() } // 「重新检测」按钮:绕过 TTL
|
|
63
|
-
|
|
64
|
-
// 登录态 → 一行人话(卡片 chip / doctor 详情共用,措辞单一事实源)。
|
|
65
|
-
function describeLogin(s) {
|
|
66
|
-
if (!s || !s.ok) return { level: 'warn', text: '未验证' + (s && s.error ? ' — ' + String(s.error).slice(0, 60) : '') }
|
|
67
|
-
if (!s.loggedIn) return { level: 'fail', text: '未登录 — 在这台机器上跑 claude auth login' }
|
|
68
|
-
const bits = ['订阅登录']
|
|
69
|
-
if (s.plan) bits.push(s.plan) // max / pro
|
|
70
|
-
if (s.email) bits.push(s.email)
|
|
71
|
-
return { level: 'ok', text: bits.join(' · ') }
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
module.exports = { loginStatus, cachedLoginStatus, clearCache, parseAuthStatus, describeLogin }
|