sintbot 1.0.9 → 1.0.10
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 +18 -0
- package/CHANGELOG.json +19 -0
- package/bin/sbot.js +34 -10
- package/package.json +1 -1
- package/src/core/engine.js +4 -3
- package/src/core/verify.js +5 -1
- package/src/health.js +2 -2
- package/src/ui/server.js +2 -2
package/CHANGELOG.en.json
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.0.10",
|
|
4
|
+
"name": "A steadier background service",
|
|
5
|
+
"items": [
|
|
6
|
+
{
|
|
7
|
+
"t": "No service without a chat platform",
|
|
8
|
+
"d": "If no Feishu, Slack or WeCom bot is connected yet, installing the background service now stops and points you to the console to connect one and bind a group first. Before, the service would exit as soon as it started."
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"t": "Keys from environment variables reach the service",
|
|
12
|
+
"d": "When the config takes keys from environment variables, installing the service now asks whether to write them into the service definition, readable only by you. Without them, models that use those keys do not work in the service."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"t": "Clearer wording",
|
|
16
|
+
"d": "Trial runs show the model from your config instead of the model's own claim (third-party models often call themselves Claude). The health check gives a missing chat platform a row of its own, and in a local terminal trial the model-switch hint says how to switch."
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
},
|
|
2
20
|
{
|
|
3
21
|
"version": "1.0.9",
|
|
4
22
|
"name": "A steadier setup wizard",
|
package/CHANGELOG.json
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.0.10",
|
|
4
|
+
"date": "2026-10-26",
|
|
5
|
+
"name": "常驻服务装得更稳",
|
|
6
|
+
"items": [
|
|
7
|
+
{
|
|
8
|
+
"t": "没接聊天平台时不装常驻服务",
|
|
9
|
+
"d": "安装时还没接飞书、Slack 或企业微信,就不再装常驻服务(装了也会一启动就退出),而是提示先在控制台里接好平台、绑群,再装服务。"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"t": "环境变量里的 key 能带进服务",
|
|
13
|
+
"d": "配置里的 key 取自环境变量时,装常驻服务会问一句要不要把它们写进服务定义(只有你自己能读);不写的话,服务里用这些 key 的模型用不了。"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"t": "说法更准",
|
|
17
|
+
"d": "试跑模型时直接显示配置里的型号,不再靠模型自报(第三方模型常自称 Claude);体检里「没配任何聊天平台」单列一行;在本机终端里试跑时,切模型的提示说清楚该怎么切。"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
},
|
|
2
21
|
{
|
|
3
22
|
"version": "1.0.9",
|
|
4
23
|
"date": "2026-10-26",
|
package/bin/sbot.js
CHANGED
|
@@ -213,7 +213,8 @@ const SUBHELP = {
|
|
|
213
213
|
+ ' 把 sintbot start --ui 装成常驻服务:崩溃自动拉起、登录后自启(macOS = launchd,Linux = systemd --user)\n'
|
|
214
214
|
+ ' install 写单元文件并立即启动;服务里固定用当前的 node、sbot 与配置文件的绝对路径,PATH 取当前 shell 的\n'
|
|
215
215
|
+ ' --dry-run 只打印要写的单元文件,不动系统;--ui-host 0.0.0.0 对局域网开控制台(须先 sintbot ui --set-password,否则拒装)\n'
|
|
216
|
-
+ ' 服务拿不到当前 shell 的环境变量:配置里 env:VAR 引用的 key 会被点名;--pass-env 把它们的值写进单元文件(文件权限 600)
|
|
216
|
+
+ ' 服务拿不到当前 shell 的环境变量:配置里 env:VAR 引用的 key 会被点名;--pass-env 把它们的值写进单元文件(文件权限 600),终端里装时不给也会问一句\n'
|
|
217
|
+
+ ' 还没接任何聊天平台时不装(退出码 3):服务会一启动就退出 —— 先 sintbot ui 在控制台接平台\n'
|
|
217
218
|
+ ' Windows 不自动装:打印 NSSM / pm2 的现成命令。日志在 ~/.sbot/logs/;同一份状态只许一个 daemon,第二个会直接报错退出',
|
|
218
219
|
version: '用法: sintbot version (也认 sbot --version / -v)\n'
|
|
219
220
|
+ ' 打印版本号',
|
|
@@ -1225,11 +1226,27 @@ async function cmdService(rest) {
|
|
|
1225
1226
|
let raw
|
|
1226
1227
|
try { raw = JSON.parse(fs.readFileSync(configPath, 'utf8')) } catch (e) { return die(`读不了配置 ${configPath}:${e.message}`) }
|
|
1227
1228
|
if (uiHost && !isLoopbackHost(uiHost) && !((raw.ui || {}).auth)) return die(`控制台对外(--ui-host ${uiHost})要先设密码:sintbot ui --set-password${values.config ? ' --config ' + values.config : ''} —— 否则服务一启动就退出、被反复拉起`)
|
|
1228
|
-
//
|
|
1229
|
+
// 一个聊天平台都没接:sintbot start 一启动就报错退出,装成服务同样是反复拉起的死循环 ⇒ 也在装之前拦。
|
|
1230
|
+
// 退出码 3 单列,install.sh 据此改口「先去控制台接平台」(env: 引用按当前 shell 解析,与服务带 --pass-env 时一致)
|
|
1231
|
+
const plat = platformsConfigured(loadConfig(configPath))
|
|
1232
|
+
if (!plat.feishu && !plat.wecom && !plat.slack) {
|
|
1233
|
+
const cfgArg = values.config ? ' --config ' + values.config : ''
|
|
1234
|
+
process.stderr.write(red(`sbot: 还没接聊天平台 —— 装成服务也会一启动就退出、被反复拉起。先运行 sintbot ui${cfgArg},在控制台「设置」里填飞书 / Slack / 企业微信的凭据并绑群(或重跑 sintbot setup),再装服务`) + '\n')
|
|
1235
|
+
process.exit(3)
|
|
1236
|
+
}
|
|
1237
|
+
// 服务进程拿不到当前 shell 的环境变量:点名配置里的 env: 引用;--pass-env 才把值写进单元文件。
|
|
1238
|
+
// 终端里装又没给 --pass-env 时问一句(默认是):不带进去,服务里用这些 key 的模型 / 平台直接用不了
|
|
1229
1239
|
const refs = svc.envRefs(raw)
|
|
1230
1240
|
const inShell = refs.filter((v) => process.env[v])
|
|
1231
1241
|
const nowhere = refs.filter((v) => !process.env[v])
|
|
1232
|
-
|
|
1242
|
+
let passEnv = !!values['pass-env']
|
|
1243
|
+
if (!passEnv && inShell.length && process.stdin.isTTY) {
|
|
1244
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr })
|
|
1245
|
+
const a = await new Promise((res) => rl.question(`${cyan('?')} 配置用到的环境变量 ${inShell.join(' ')} 服务里拿不到 —— 把它们的值写进服务定义吗?(文件权限 600;以后换 key 要重跑这条命令) ${dim('(Y/n)')} `, res))
|
|
1246
|
+
rl.close()
|
|
1247
|
+
passEnv = !/^n/i.test(String(a).trim())
|
|
1248
|
+
}
|
|
1249
|
+
const env = passEnv ? Object.fromEntries(inShell.map((v) => [v, process.env[v]])) : {}
|
|
1233
1250
|
const tools = ['claude', 'codex'].map((n) => whichSync(n)).filter(Boolean)
|
|
1234
1251
|
const node = svc.stableNodeBin({ execPath: process.execPath, candidates: [whichSync('node')] })
|
|
1235
1252
|
const sbotBin = path.resolve(__dirname, 'sbot.js')
|
|
@@ -1244,7 +1261,7 @@ async function cmdService(rest) {
|
|
|
1244
1261
|
})
|
|
1245
1262
|
if (node.warn) process.stderr.write(yellow('⚠ ' + node.warn) + '\n')
|
|
1246
1263
|
if (svc.versionedPath(sbotBin)) process.stderr.write(yellow(`⚠ sbot 装在带版本号的 node 目录下(${sbotBin}),换 node 版本后要重装 sintbot 并重跑 sintbot service install`) + '\n')
|
|
1247
|
-
if (inShell.length && !
|
|
1264
|
+
if (inShell.length && !passEnv) process.stderr.write(yellow(`⚠ 配置引用的环境变量 ${inShell.join(' ')} 只在当前终端里有,服务里拿不到 —— 加 --pass-env 把值写进服务定义(文件权限 600),或把值直接写进配置(配置本身是 600)`) + '\n')
|
|
1248
1265
|
if (nowhere.length) process.stderr.write(yellow(`⚠ 配置引用的环境变量 ${nowhere.join(' ')} 当前终端里也没有 —— 用到它们的模型 / 平台不会工作`) + '\n')
|
|
1249
1266
|
if (platform !== 'darwin' && platform !== 'linux') { process.stdout.write(svc.windowsHint(spec) + '\n'); return }
|
|
1250
1267
|
if (!tools.length) process.stderr.write(yellow('⚠ PATH 里没找到 claude / codex —— 服务能起,但群里的轮次会失败;装好后重跑 sintbot service install') + '\n')
|
|
@@ -1418,7 +1435,7 @@ async function cmdSetup(rest) {
|
|
|
1418
1435
|
const readyKeyed = () => Object.keys(raw.providers).filter((n) => raw.providers[n].baseUrl && !raw.providers[n].hidden && setupmodels.keyPresent(raw.providers[n].apiKey))
|
|
1419
1436
|
const verified = [] // 本次向导里试跑通过的
|
|
1420
1437
|
const tried = new Set() // 本次试跑过的(不论成败),结尾不再重复问
|
|
1421
|
-
// 试跑一轮(与控制台「模型 → 测试」同一个 runTurn)
|
|
1438
|
+
// 试跑一轮(与控制台「模型 → 测试」同一个 runTurn 和提示词):不用工具,只要一句回复。
|
|
1422
1439
|
// 关掉 claude 的自动重试:key 错时它默认反复重试约 3 分钟才放弃,关掉后半秒内就拿到 401 原文
|
|
1423
1440
|
const tryTurn = async (name) => {
|
|
1424
1441
|
const p = raw.providers[name]
|
|
@@ -1428,7 +1445,7 @@ async function cmdSetup(rest) {
|
|
|
1428
1445
|
const t0 = Date.now()
|
|
1429
1446
|
try {
|
|
1430
1447
|
const { result, aborted } = await runner.runTurn({
|
|
1431
|
-
bin: claudeBin || claudeName, prompt:
|
|
1448
|
+
bin: claudeBin || claudeName, prompt: verify.TRIAL_PROMPT, provider, model: provider.model,
|
|
1432
1449
|
workdir: os.tmpdir(), permissionMode: (raw.agent && raw.agent.permissionMode) || 'dontAsk', signal: ac.signal,
|
|
1433
1450
|
extraEnv: { CLAUDE_CODE_MAX_RETRIES: '0' },
|
|
1434
1451
|
})
|
|
@@ -1436,13 +1453,14 @@ async function cmdSetup(rest) {
|
|
|
1436
1453
|
if (aborted || ac.signal.aborted) return { ok: false, text: `timeout:${Math.round((Date.now() - t0) / 1000)} 秒内没有回复`, ms: Date.now() - t0 }
|
|
1437
1454
|
const ok = !!text && !(result && result.isError)
|
|
1438
1455
|
verify.recordTurn(provider, ok, text) // doctor / 控制台据此报「能用」或「key 不对」
|
|
1439
|
-
return { ok, text: text || '没有返回内容', ms: Date.now() - t0 }
|
|
1456
|
+
return { ok, text: text || '没有返回内容', ms: Date.now() - t0, model: provider.model }
|
|
1440
1457
|
} catch (e) {
|
|
1441
1458
|
verify.recordTurn(provider, false, (e && e.message) || String(e))
|
|
1442
1459
|
return { ok: false, text: (e && e.message) || String(e), ms: Date.now() - t0 }
|
|
1443
1460
|
} finally { clearTimeout(timer) }
|
|
1444
1461
|
}
|
|
1445
|
-
|
|
1462
|
+
// 型号取配置里的(r.model),不靠模型自报:走 Claude Code 的第三方模型常自称 Claude,新用户会以为没切成
|
|
1463
|
+
const reportTurn = (label, r) => process.stdout.write(` ${cyan('✓')} ${label}${r.model ? ' · ' + r.model : ''} 能用(${(r.ms / 1000).toFixed(1)} 秒):${r.text.replace(/\s+/g, ' ').slice(0, 80)}\n`)
|
|
1446
1464
|
// 配一家用 API key 的:选站点 → 告诉去哪拿 key → 粘贴 → 当场试跑,失败说人话、可重试
|
|
1447
1465
|
const configureKeyed = async (name) => {
|
|
1448
1466
|
const info = setupmodels.PROVIDER_INFO[name] || { label: name }
|
|
@@ -1801,10 +1819,16 @@ async function cmdSetup(rest) {
|
|
|
1801
1819
|
const cfg = loadConfig(cfgPath)
|
|
1802
1820
|
await runDoctor(cfg)
|
|
1803
1821
|
const cfgArg = cfgPath === path.join(os.homedir(), '.sbot', 'config.json') ? '' : ` --config ${cfgPath}`
|
|
1822
|
+
// 一个平台都没接时守护进程起不来(装服务会被拦下):先去控制台接平台,再装服务
|
|
1823
|
+
const plat = platformsConfigured(cfg)
|
|
1824
|
+
const noPlatform = !plat.feishu && !plat.wecom && !plat.slack
|
|
1804
1825
|
process.stdout.write('\n' + gray('— 下一步 —') + '\n'
|
|
1805
1826
|
+ ` sintbot agent run --provider ${cfg.defaultProvider} "用一句话自我介绍"${cfgArg} ${dim('先在终端试一轮')}\n`
|
|
1806
|
-
+
|
|
1807
|
-
|
|
1827
|
+
+ (noPlatform
|
|
1828
|
+
? ` sintbot ui${cfgArg} ${dim('还没接聊天平台:在控制台「设置」里填飞书 / Slack / 企业微信的凭据、绑群')}\n`
|
|
1829
|
+
+ ` sintbot service install${cfgArg} ${dim('接好平台后,装成常驻服务(开机/登录自启,崩溃自动拉起)')}\n`
|
|
1830
|
+
: ` sintbot service install${cfgArg} ${dim('装成常驻服务(开机/登录自启,崩溃自动拉起)')}\n`
|
|
1831
|
+
+ ` sintbot ui${cfgArg} ${dim('打开控制台:绑群、切模型、看实时活动')}\n`)
|
|
1808
1832
|
+ (!((cfg.providers || {})[cfg.defaultProvider] || {}).baseUrl
|
|
1809
1833
|
? '\n' + yellow('⚠ 订阅条款:') + '用个人的 Claude / ChatGPT 订阅登录来服务多人群聊,可能不符合提供方的使用条款;团队使用请改用 API key 或企业账号,并自行确认合规。\n' : ''))
|
|
1810
1834
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sintbot",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "Drive headless Claude Code (or Codex) from Slack / Feishu / WeCom group chats — one group, one project, swappable model brain. A standalone daemon, not a Claude Code plugin.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"sbot": "bin/sbot.js",
|
package/src/core/engine.js
CHANGED
|
@@ -203,10 +203,11 @@ const T = {
|
|
|
203
203
|
// ⚠ 「唯一写入方」这条约束没作废,只是换了兑现方式:群内切走的是**与控制台同一套写入动作**
|
|
204
204
|
// (配置落盘 + 运行时覆盖 + 清会话一起做),不是新开一条写法。
|
|
205
205
|
// `current`/`list` 都是预格式化的「模型名 · 型号」,免得把名字与型号混为一谈。
|
|
206
|
-
//
|
|
206
|
+
// 没有注入写入器只发生在 CLI 试跑(sintbot chat;守护进程总会注入):如实说这里切不了、群里能切,
|
|
207
|
+
// 别给一条按了没反应的指令,也别说成「群内不支持」(1.0.9 及以前那样说,读起来像群里也切不了)。
|
|
207
208
|
modelInfo: (current, list, canSwitch) => `【当前模型】${current}\n【可用】${list}\n` + (canSwitch
|
|
208
209
|
? '换模型:发「/model 名字」,例如 /model volcano\n⚠ 换完是**新的一段对话**,之前聊的内容不再带着'
|
|
209
|
-
: '
|
|
210
|
+
: '这里是终端试跑,不能切换 —— 换模型用 sintbot chat --provider 名字;在群里发「/model 名字」就能切'),
|
|
210
211
|
modelUnknown: (want, names) => `⚠ 没有「${want}」这个模型\n【可用】${names}\n发「/model 名字」换,名字要一模一样`,
|
|
211
212
|
modelSame: (current) => `本群已经在用 ${current},没有变化`,
|
|
212
213
|
// ⚠ 忙的时候不切:换模型要清会话,而当前这轮正靠它跑;悄悄换掉等于把人手头的活废了。
|
|
@@ -485,7 +486,7 @@ function helpText(group, provider, hasImagen, caps = {}) {
|
|
|
485
486
|
'· /skip — 只放弃手头这件,排队的继续;之前聊的还记得',
|
|
486
487
|
'· /stop — 手头和排队的全取消;之前聊的还记得',
|
|
487
488
|
'· /new — 全取消,并忘掉之前聊的,从头开始',
|
|
488
|
-
'· /model — 本群在用的模型' + (canSwitchModel ? ';换:/model 名字(换完之前聊的不再带着)' : '(
|
|
489
|
+
'· /model — 本群在用的模型' + (canSwitchModel ? ';换:/model 名字(换完之前聊的不再带着)' : '(终端试跑里不能切,换用 --provider)'),
|
|
489
490
|
// 压缩那半句(v0.11.92):群里会冒出「🔄 会话已自动压缩 + 额度」这条通知,/help 里得有它的出处 ——
|
|
490
491
|
// 否则第一次见到的人只会问「这是什么」;放在 /hud 这行,因为会话规模正是它管的。
|
|
491
492
|
'· /hud — 额度与本会话用量;聊太长会自动压缩,群里会提示',
|
package/src/core/verify.js
CHANGED
|
@@ -82,4 +82,8 @@ function when(at, now = Date.now()) {
|
|
|
82
82
|
return d.toDateString() === n.toDateString() ? `今天 ${pad(d.getHours())}:${pad(d.getMinutes())}` : `${d.getMonth() + 1} 月 ${d.getDate()} 日`
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
// 试跑一轮用的提示词(向导试跑与控制台「模型 → 测试」共用)。不让模型自报身份:走 Claude Code 的第三方模型
|
|
86
|
+
// 常照系统提示自称 Claude,新用户会以为没切成;型号由调用方按配置显示
|
|
87
|
+
const TRIAL_PROMPT = '用一句话回复,说明你已准备好开始工作。'
|
|
88
|
+
|
|
89
|
+
module.exports = { fingerprint, providerFp, feishuFp, slackFp, lookup, record, recordTurn, recordPrecheck, when, REFRESH_MS, TRIAL_PROMPT }
|
package/src/health.js
CHANGED
|
@@ -277,10 +277,10 @@ async function checkHealth(cfg, opts = {}) {
|
|
|
277
277
|
if (!cfg.feishu.botOpenId) add('warn', 'feishu.botOpenId', (cfg.feishu.onlyWhenMentioned
|
|
278
278
|
? '没填 — 开着「只在 @ 时回复」却认不出哪个 @ 是机器人,群里一条都不会回'
|
|
279
279
|
: '没填 — 分不清 @机器人 和 @别人,群里 @ 别人的消息也会接') + ';重跑 sintbot setup 选飞书预检可自动填')
|
|
280
|
-
} else if (!hasId && !hasSecret) { if (!plat.wecom && !plat.slack) add('warn', '
|
|
280
|
+
} else if (!hasId && !hasSecret) { if (!plat.wecom && !plat.slack) add('warn', 'platforms', '没配任何聊天平台 — sintbot start 收不到消息') }
|
|
281
281
|
else add('fail', 'feishu', hasId ? '缺 appSecret' : '缺 appId')
|
|
282
282
|
} else if (!plat.wecom && !plat.slack) {
|
|
283
|
-
add('warn', '
|
|
283
|
+
add('warn', 'platforms', '没配任何聊天平台 — sintbot start 收不到消息')
|
|
284
284
|
}
|
|
285
285
|
// 写了 wecom 块就要报:env: 引用解析成空串也算缺(真机踩到 —— 非交互 shell 没 source 凭据文件,doctor 一声不吭地把群报成「没配企微」)
|
|
286
286
|
if (cfg.wecom) {
|
package/src/ui/server.js
CHANGED
|
@@ -627,11 +627,11 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
627
627
|
try {
|
|
628
628
|
const { result } = await runTurn({
|
|
629
629
|
bin: cfg.agent.bin,
|
|
630
|
-
prompt: prompt ||
|
|
630
|
+
prompt: prompt || verify.TRIAL_PROMPT,
|
|
631
631
|
provider,
|
|
632
632
|
model: provider.model,
|
|
633
633
|
permissionMode: cfg.agent.permissionMode,
|
|
634
|
-
signal: ac.signal, // a one-line
|
|
634
|
+
signal: ac.signal, // a one-line reply — needs no tools, so no allow list
|
|
635
635
|
})
|
|
636
636
|
const ok = !(result && result.isError)
|
|
637
637
|
verify.recordTurn(provider, ok, (result && result.text) || '', vopts) // doctor / 概览据此报「能用」或「key 不对」
|