sintbot 1.4.17 → 1.4.18
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 +31 -0
- package/CHANGELOG.json +31 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/bin/sintbot-v2.js +65 -2
- package/docs/AI-ASSISTED-INSTALL.md +3 -3
- package/docs/install.md +27 -11
- package/docs/zh-CN/AI-ASSISTED-INSTALL.md +3 -3
- package/docs/zh-CN/install.md +28 -12
- package/install.sh +1 -1
- package/package.json +2 -1
- package/src/agent/claudecode.js +41 -3
- package/src/agent/claudeusage.js +53 -4
- package/src/agent/privatecore.js +25 -4
- package/src/agent/privatecore.wasm +0 -0
- package/src/agent/runner.js +9 -1
- package/src/c2configcontrol.js +7 -0
- package/src/c2console.js +3 -0
- package/src/c2multigroupservice.js +1 -1
- package/src/connect.js +100 -22
- package/src/core/c2runnerinput.js +47 -1
- package/src/core/v2engine.js +211 -11
- package/src/setupserver.js +447 -0
- package/src/ui/console.html +10 -7
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// Setup mode: `sintbot start --ui` with no configuration serves a local page
|
|
3
|
+
// that runs the scan-to-connect flow in this process. The page collects the
|
|
4
|
+
// inputs `sintbot connect` would take as flags, shows the QR code, streams the
|
|
5
|
+
// step notices, and offers the resume paths (existing app, hand-made group)
|
|
6
|
+
// as buttons. Once the configuration is written and initialized, the server
|
|
7
|
+
// closes and the caller continues into the normal start.
|
|
8
|
+
//
|
|
9
|
+
// Everything that decides the flow stays in src/connect.js and the core; this
|
|
10
|
+
// module only adapts it to HTTP: arguments in, captured notices out.
|
|
11
|
+
const http = require('node:http')
|
|
12
|
+
const path = require('node:path')
|
|
13
|
+
const { spawnSync } = require('node:child_process')
|
|
14
|
+
const { runConnect, qrPage, probeRunners: connectProbe, EXIT_INTERRUPTED } = require('./connect')
|
|
15
|
+
|
|
16
|
+
const QR_URL = /^https:\/\/[^\s]+/
|
|
17
|
+
const APP_ID_LINE = /^应用 ID:(cli_[A-Za-z0-9]{6,64})$/m
|
|
18
|
+
const CHAT_HINT = /--chat <oc_…>/
|
|
19
|
+
const APP_ID = /^cli_[A-Za-z0-9]{6,64}$/
|
|
20
|
+
const CHAT_ID = /^oc_[A-Za-z0-9_-]{1,124}$/
|
|
21
|
+
const PROJECT_NAME = /^[\p{L}\p{N}][\p{L}\p{N}._-]{0,59}$/u
|
|
22
|
+
const RUNNERS = Object.freeze(['codex', 'claude'])
|
|
23
|
+
const BODY_LIMIT = 16 * 1024
|
|
24
|
+
|
|
25
|
+
// The same probe runConnect uses, so the page never offers a runner the run
|
|
26
|
+
// would then refuse.
|
|
27
|
+
function probeRunners(spawn = spawnSync, env = process.env) {
|
|
28
|
+
try { return connectProbe(spawn, env) } catch { return Object.fromEntries(RUNNERS.map(r => [r, false])) }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function svgOf(url) {
|
|
32
|
+
const m = /<svg[\s\S]*?<\/svg>/.exec(qrPage(url))
|
|
33
|
+
return m ? m[0] : ''
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// One attempt = one `runConnect` run with captured output. The state object
|
|
37
|
+
// is what the page polls; it carries no secrets (the app secret never reaches
|
|
38
|
+
// stdout/stderr by design of runConnect).
|
|
39
|
+
function createSetupSession({ configFile, dataDir, home, version, runners, connect = runConnect, deps = {},
|
|
40
|
+
initialize = (file, options) => require('./c2install').initializeInstallation(file, options), log = () => {} }) {
|
|
41
|
+
const state = {
|
|
42
|
+
phase: 'form', // form | running | waitingChat | done | failed
|
|
43
|
+
lines: [], // notices in order (stderr of runConnect)
|
|
44
|
+
qr: null, // inline SVG once the scan link is known
|
|
45
|
+
appId: null, // printed by runConnect after the scan; reused by every retry
|
|
46
|
+
notice: '', // last notice when stopped or failed
|
|
47
|
+
groupAppId: null, // app owning the confirmed group
|
|
48
|
+
chatId: null, // a confirmed created group, reused after cancellation or retry
|
|
49
|
+
initOnly: false, // the configuration is written; only the journals failed
|
|
50
|
+
modelDefaults: null, // the live recommendation confirmed before scanning
|
|
51
|
+
runners, // which executors answered --version
|
|
52
|
+
defaults: { name: 'sintbot', projectsRoot: path.join(home, 'Projects') },
|
|
53
|
+
startedAt: null, endedAt: null,
|
|
54
|
+
}
|
|
55
|
+
let signalHandlers = new Map()
|
|
56
|
+
let current = null
|
|
57
|
+
let finish
|
|
58
|
+
const completed = new Promise(resolve => { finish = resolve })
|
|
59
|
+
|
|
60
|
+
const argvFor = (input) => {
|
|
61
|
+
const argv = ['--config', configFile, '--no-open']
|
|
62
|
+
if (dataDir) argv.push('--data-dir', dataDir)
|
|
63
|
+
if (input.workdir) argv.push('--workdir', input.workdir)
|
|
64
|
+
else argv.push('--name', input.name || 'sintbot')
|
|
65
|
+
if (input.provider) argv.push('--provider', input.provider)
|
|
66
|
+
if (input.basic) argv.push('--basic')
|
|
67
|
+
if (input.appId) argv.push('--app-id', input.appId)
|
|
68
|
+
if (input.chatId) argv.push('--chat', input.chatId)
|
|
69
|
+
return argv
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const validate = (input) => {
|
|
73
|
+
if (!input || typeof input !== 'object') throw new Error('请填写接入信息')
|
|
74
|
+
const out = {}
|
|
75
|
+
if (input.workdir !== undefined && input.workdir !== '') {
|
|
76
|
+
if (typeof input.workdir !== 'string' || !path.isAbsolute(input.workdir)) throw new Error('项目目录必须是绝对路径')
|
|
77
|
+
out.workdir = path.resolve(input.workdir)
|
|
78
|
+
} else {
|
|
79
|
+
const name = input.name === undefined || input.name === '' ? 'sintbot' : String(input.name)
|
|
80
|
+
if (!PROJECT_NAME.test(name) || name === '.' || name === '..') throw new Error('项目名只能用字母、数字、"."、"_" 和 "-"(最长 60)')
|
|
81
|
+
out.name = name
|
|
82
|
+
}
|
|
83
|
+
if (input.provider !== undefined && input.provider !== '' && input.provider !== 'auto') {
|
|
84
|
+
if (!RUNNERS.includes(input.provider)) throw new Error('执行器只能是 codex 或 claude')
|
|
85
|
+
out.provider = input.provider
|
|
86
|
+
}
|
|
87
|
+
out.basic = input.basic === true
|
|
88
|
+
if (input.appId !== undefined && input.appId !== '') {
|
|
89
|
+
if (!APP_ID.test(input.appId)) throw new Error('应用 ID 应形如 cli_…')
|
|
90
|
+
out.appId = input.appId
|
|
91
|
+
}
|
|
92
|
+
if (input.chatId !== undefined && input.chatId !== '') {
|
|
93
|
+
if (!CHAT_ID.test(input.chatId)) throw new Error('群 ID 应形如 oc_…')
|
|
94
|
+
out.chatId = input.chatId
|
|
95
|
+
}
|
|
96
|
+
return out
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function start(rawInput) {
|
|
100
|
+
if (state.phase === 'running') throw new Error('接入正在进行')
|
|
101
|
+
if (state.phase === 'done') throw new Error('接入已完成')
|
|
102
|
+
if (state.initOnly) return retryInit()
|
|
103
|
+
// A resume (hand-made group) keeps the directory and options of the run
|
|
104
|
+
// that failed, even if the page was reloaded in between.
|
|
105
|
+
if (rawInput && rawInput.chatId && state.input) {
|
|
106
|
+
const keep = state.input
|
|
107
|
+
rawInput = { name: keep.name || undefined, workdir: keep.workdir || undefined, provider: keep.provider || undefined, basic: keep.basic, ...rawInput }
|
|
108
|
+
for (const k of ['name', 'workdir', 'provider']) if (rawInput[k] === '' || rawInput[k] === null) rawInput[k] = keep[k] || undefined
|
|
109
|
+
}
|
|
110
|
+
const input = validate(rawInput)
|
|
111
|
+
// Every retry reuses the app the first scan created: no second app in the
|
|
112
|
+
// organization, and the same --app-id resume the terminal route offers.
|
|
113
|
+
if (!input.appId && state.appId) input.appId = state.appId
|
|
114
|
+
if (!input.chatId && state.chatId && input.appId === state.groupAppId) input.chatId = state.chatId
|
|
115
|
+
state.phase = 'running'
|
|
116
|
+
state.modelDefaults = null
|
|
117
|
+
state.lines = []
|
|
118
|
+
state.qr = null
|
|
119
|
+
state.notice = ''
|
|
120
|
+
state.startedAt = Date.now()
|
|
121
|
+
state.endedAt = null
|
|
122
|
+
state.input = { name: input.name || null, workdir: input.workdir || null, provider: input.provider || null, basic: input.basic }
|
|
123
|
+
signalHandlers = new Map()
|
|
124
|
+
const stderrText = { buf: '' }
|
|
125
|
+
const write = (sink) => (chunk) => {
|
|
126
|
+
sink.buf += String(chunk)
|
|
127
|
+
let nl
|
|
128
|
+
while ((nl = sink.buf.indexOf('\n')) >= 0) {
|
|
129
|
+
const line = sink.buf.slice(0, nl)
|
|
130
|
+
sink.buf = sink.buf.slice(nl + 1)
|
|
131
|
+
sink.onLine(line)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const stdout = { buf: '', onLine: line => {
|
|
135
|
+
if (QR_URL.test(line)) { if (!state.qr) { state.qr = svgOf(line); state.lines.push('请用手机飞书扫描页面上的二维码。') }; return }
|
|
136
|
+
// The final notice arrives on stdout; the start command does not apply here.
|
|
137
|
+
if (line.trim() && !/\bstart --config\b/.test(line)) state.lines.push(line)
|
|
138
|
+
} }
|
|
139
|
+
stderrText.onLine = line => {
|
|
140
|
+
if (!line.trim()) return
|
|
141
|
+
state.lines.push(line)
|
|
142
|
+
const m = APP_ID_LINE.exec(line)
|
|
143
|
+
if (m) { state.appId = m[1]; state.qr = null }
|
|
144
|
+
}
|
|
145
|
+
const run = (async () => {
|
|
146
|
+
let code
|
|
147
|
+
try {
|
|
148
|
+
code = await connect(argvFor(input), { ...deps, stdout: { write: write(stdout) }, stderr: { write: write(stderrText) },
|
|
149
|
+
isTTY: false, version, env: process.env, home,
|
|
150
|
+
onModelDefaults: value => { state.modelDefaults = { provider: value.provider, model: value.model, effort: value.effort, name: value.name, source: value.source } },
|
|
151
|
+
onGroupReady: ({ appId, chatId }) => { state.appId = appId; state.groupAppId = appId; state.chatId = chatId },
|
|
152
|
+
signals: { on: (sig, fn) => signalHandlers.set(sig, fn), off: (sig) => signalHandlers.delete(sig) } })
|
|
153
|
+
} catch (error) {
|
|
154
|
+
code = 1
|
|
155
|
+
state.lines.push(error?.message || String(error))
|
|
156
|
+
}
|
|
157
|
+
if (stdout.buf.trim()) stdout.onLine(stdout.buf)
|
|
158
|
+
if (stderrText.buf.trim()) stderrText.onLine(stderrText.buf)
|
|
159
|
+
if (code === EXIT_INTERRUPTED) state.lines.push('已取消,没有写入任何文件。')
|
|
160
|
+
state.qr = null
|
|
161
|
+
state.endedAt = Date.now()
|
|
162
|
+
const last = [...state.lines].reverse().find(l => l.trim()) || ''
|
|
163
|
+
state.notice = last
|
|
164
|
+
if (code === 0) {
|
|
165
|
+
state.phase = 'done'
|
|
166
|
+
log('setup: configuration written and initialized')
|
|
167
|
+
finish(true)
|
|
168
|
+
} else if (state.appId && state.lines.some(l => CHAT_HINT.test(l))) {
|
|
169
|
+
state.phase = 'waitingChat'
|
|
170
|
+
} else {
|
|
171
|
+
state.phase = 'failed'
|
|
172
|
+
// The configuration exists but the journals do not: retry only that step.
|
|
173
|
+
if (state.lines.some(l => /^初始化失败/.test(l))) state.initOnly = true
|
|
174
|
+
}
|
|
175
|
+
current = null
|
|
176
|
+
})()
|
|
177
|
+
current = run
|
|
178
|
+
return state
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function retryInit() {
|
|
182
|
+
state.phase = 'running'; state.notice = ''; state.startedAt = Date.now()
|
|
183
|
+
const run = (async () => {
|
|
184
|
+
try {
|
|
185
|
+
initialize(configFile, { dataDir })
|
|
186
|
+
state.initOnly = false
|
|
187
|
+
state.lines.push('初始化完成。')
|
|
188
|
+
state.phase = 'done'
|
|
189
|
+
state.endedAt = Date.now()
|
|
190
|
+
finish(true)
|
|
191
|
+
} catch (error) {
|
|
192
|
+
state.lines.push(`初始化失败(${error?.message || error})。`)
|
|
193
|
+
state.notice = state.lines[state.lines.length - 1]
|
|
194
|
+
state.phase = 'failed'
|
|
195
|
+
state.endedAt = Date.now()
|
|
196
|
+
}
|
|
197
|
+
current = null
|
|
198
|
+
})()
|
|
199
|
+
current = run
|
|
200
|
+
return state
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Signals the run exactly like Ctrl-C; runConnect stops at its next step
|
|
204
|
+
// boundary and writes nothing. False when nothing can be cancelled.
|
|
205
|
+
function cancel() {
|
|
206
|
+
if (state.phase !== 'running') return false
|
|
207
|
+
const handler = signalHandlers.get('SIGINT') || signalHandlers.get('SIGTERM')
|
|
208
|
+
if (!handler) return false
|
|
209
|
+
handler()
|
|
210
|
+
return true
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function reset() {
|
|
214
|
+
if (state.phase === 'running' || state.phase === 'done') return false
|
|
215
|
+
state.phase = 'form'; state.lines = []; state.qr = null; state.notice = ''
|
|
216
|
+
return true
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const snapshot = () => ({ ...state, lines: state.lines.slice(-60) })
|
|
220
|
+
void EXIT_INTERRUPTED
|
|
221
|
+
return { state, snapshot, start, cancel, reset, completed, wait: () => current }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function createSetupServer(options) {
|
|
225
|
+
const { host = '127.0.0.1', port = 4600, version = '', log = () => {}, page = setupPage } = options
|
|
226
|
+
if (!['127.0.0.1', 'localhost', '::1'].includes(host)) throw new Error('Setup mode only listens on this Mac')
|
|
227
|
+
const session = createSetupSession(options)
|
|
228
|
+
const json = (res, status, body) => {
|
|
229
|
+
const text = JSON.stringify(body)
|
|
230
|
+
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store', 'content-length': Buffer.byteLength(text) })
|
|
231
|
+
res.end(text)
|
|
232
|
+
}
|
|
233
|
+
const readBody = (req) => new Promise((resolve, reject) => {
|
|
234
|
+
let size = 0, raw = ''
|
|
235
|
+
if (Number(req.headers['content-length']) > BODY_LIMIT) { const e = new Error('请求过大'); e.status = 413; return reject(e) }
|
|
236
|
+
req.on('data', chunk => { size += chunk.length; if (size > BODY_LIMIT) { const e = new Error('请求过大'); e.status = 413; reject(e) } else raw += chunk })
|
|
237
|
+
req.on('end', () => { try { resolve(raw ? JSON.parse(raw) : {}) } catch { reject(new Error('请求不是 JSON')) } })
|
|
238
|
+
req.on('error', reject)
|
|
239
|
+
})
|
|
240
|
+
let boundPort = port
|
|
241
|
+
const localHost = value => /^(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/.test(value || '') &&
|
|
242
|
+
(Number((value.match(/:(\d+)$/) || [])[1] || 80) === boundPort)
|
|
243
|
+
const server = http.createServer(async (req, res) => {
|
|
244
|
+
try {
|
|
245
|
+
// Local page only: the Host must be this listener (no DNS rebinding) and
|
|
246
|
+
// a browser Origin, when present, must be the same host and port.
|
|
247
|
+
if (!localHost(req.headers.host)) return json(res, 403, { error: 'forbidden' })
|
|
248
|
+
const origin = req.headers.origin
|
|
249
|
+
if (origin) {
|
|
250
|
+
let originHost = null
|
|
251
|
+
try { const parsed = new URL(origin); if (parsed.protocol === 'http:') originHost = parsed.host } catch {}
|
|
252
|
+
if (!originHost || originHost !== req.headers.host) return json(res, 403, { error: 'forbidden' })
|
|
253
|
+
}
|
|
254
|
+
const url = new URL(req.url, 'http://127.0.0.1')
|
|
255
|
+
if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/setup')) {
|
|
256
|
+
const html = page({ version })
|
|
257
|
+
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' })
|
|
258
|
+
return res.end(html)
|
|
259
|
+
}
|
|
260
|
+
if (req.method === 'GET' && url.pathname === '/api/setup/state') return json(res, 200, session.snapshot())
|
|
261
|
+
if (req.method === 'POST' && url.pathname === '/api/setup/start') {
|
|
262
|
+
if (!/^application\/json/.test(req.headers['content-type'] || '')) return json(res, 415, { error: '需要 JSON' })
|
|
263
|
+
const body = await readBody(req)
|
|
264
|
+
return json(res, 200, { ok: true, state: session.start(body) && session.snapshot() })
|
|
265
|
+
}
|
|
266
|
+
if (req.method === 'POST' && url.pathname === '/api/setup/cancel') return json(res, 200, { ok: session.cancel() })
|
|
267
|
+
if (req.method === 'POST' && url.pathname === '/api/setup/reset') return json(res, 200, { ok: session.reset() })
|
|
268
|
+
if (req.method === 'GET') { res.writeHead(302, { location: '/' }); return res.end() }
|
|
269
|
+
return json(res, 404, { error: 'not found' })
|
|
270
|
+
} catch (error) {
|
|
271
|
+
if (res.headersSent || res.destroyed) return
|
|
272
|
+
return json(res, error?.status || 400, { error: error?.message || String(error) })
|
|
273
|
+
}
|
|
274
|
+
})
|
|
275
|
+
server.keepAliveTimeout = 1000
|
|
276
|
+
server.on('clientError', (error, socket) => {
|
|
277
|
+
if (!socket.destroyed) socket.end('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n')
|
|
278
|
+
})
|
|
279
|
+
return {
|
|
280
|
+
server, session,
|
|
281
|
+
listen: () => new Promise((resolve, reject) => {
|
|
282
|
+
server.once('error', reject)
|
|
283
|
+
server.listen(port, host, () => {
|
|
284
|
+
server.off('error', reject)
|
|
285
|
+
boundPort = server.address().port
|
|
286
|
+
server.on('error', error => log(`setup server error: ${error.message}`))
|
|
287
|
+
resolve(server.address())
|
|
288
|
+
})
|
|
289
|
+
}),
|
|
290
|
+
// Resolves once the configuration is written; the server is then closed
|
|
291
|
+
// after a short grace so the page can read the final state.
|
|
292
|
+
completed: session.completed,
|
|
293
|
+
close: () => new Promise(resolve => { server.closeAllConnections?.(); server.close(() => resolve()) }),
|
|
294
|
+
log,
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Display-only translations: the core's original notices still drive the session.
|
|
299
|
+
// Unknown upstream details retain machine-readable error codes without mixing UI languages.
|
|
300
|
+
function translateSetupNotice(value, lang = 'en') {
|
|
301
|
+
const line = String(value || '')
|
|
302
|
+
if (lang === 'zh' && /^Could not read live model defaults for /.test(line)) return '无法读取执行器实时推荐的型号和思考深度。请检查执行器登录和网络后重试;本次没有新建飞书应用。'
|
|
303
|
+
if (lang === 'zh' && /^The live model catalog did not provide/.test(line)) return '实时模型清单没有提供有效的型号和思考深度,请检查执行器登录后重试。'
|
|
304
|
+
if (lang === 'zh') return line.replace(/然后执行 sintbot connect.*$/, '然后在此页面填写群 ID 并继续。')
|
|
305
|
+
.replace(/重新执行 sintbot connect(?: --app-id \S+)?[。]?/g, '在此页面重试。')
|
|
306
|
+
.replace(/可手动执行 sintbot init 重试。/, '请点击「重试初始化」。')
|
|
307
|
+
const exact = {
|
|
308
|
+
'请填写接入信息': 'Enter the project details.',
|
|
309
|
+
'项目目录必须是绝对路径': 'Enter an absolute path for the project folder.',
|
|
310
|
+
'项目名只能用字母、数字、"."、"_" 和 "-"(最长 60)': 'Use letters, digits, ".", "_" or "-" for the project name (up to 60 characters).',
|
|
311
|
+
'执行器只能是 codex 或 claude': 'Choose Codex or Claude Code.',
|
|
312
|
+
'应用 ID 应形如 cli_…': 'An app ID must start with cli_.',
|
|
313
|
+
'群 ID 应形如 oc_…': 'A group ID must start with oc_.',
|
|
314
|
+
'正在读取执行器推荐的型号和思考深度…': 'Reading the executor’s recommended model and thinking depth…',
|
|
315
|
+
'接入正在进行': 'Setup is already running.', '接入已完成': 'Setup is already complete.',
|
|
316
|
+
'请求过大': 'The request is too large.', '请求不是 JSON': 'The request is not valid JSON.', '需要 JSON': 'Send a JSON request.',
|
|
317
|
+
'请用手机飞书扫描页面上的二维码。': 'Scan this QR code with Feishu on your phone.',
|
|
318
|
+
'请先确认手机飞书当前在企业/团队身份下;没有组织的个人身份扫不了,需先新建一个团队(免费)。': 'Switch Feishu on your phone to a company or team account. A personal account without an organization cannot connect; create a free team first.',
|
|
319
|
+
'等待扫码…': 'Waiting for you to scan…', '等待扫码…(已放慢轮询)': 'Waiting for you to scan… (checking less often)',
|
|
320
|
+
'已切换到 Lark 国际版域名。': 'Switched to the international Lark service.',
|
|
321
|
+
'已取消,没有写入任何文件。': 'Cancelled. No configuration or project files were written.',
|
|
322
|
+
'已获取应用凭证。': 'App credentials received.', '应用已启用。': 'The app is enabled.',
|
|
323
|
+
'当前是个人身份,飞书不允许它调用接口。请新建一个团队,切换过去后重新扫码。': 'Feishu does not allow this personal account to use the API. Create a team, switch to it, and scan again.',
|
|
324
|
+
'已提交,等组织管理员审核通过。': 'Submitted. Waiting for your organization administrator to approve the app.',
|
|
325
|
+
'应用未启用:请到飞书开发者后台启用该应用后重新执行 sintbot connect。': 'Enable the app in the Feishu developer console, then retry here.',
|
|
326
|
+
'自检通过。': 'Readiness checks passed.', '基础模式,跳过权限开通。': 'Basic setup: skipping permission changes.',
|
|
327
|
+
'基础模式,跳过发布。': 'Basic setup: skipping publication.', '权限已开通。': 'Permissions enabled.',
|
|
328
|
+
'已提交发布。': 'App publication submitted.', '使用你提供的群。': 'Using the group you provided.',
|
|
329
|
+
'群已创建并拉入机器人。': 'Group created and the bot added.', '初始化完成。': 'Initialization complete.',
|
|
330
|
+
'连接完成。': 'Connected.', '连接完成,去飞书群里 @机器人 试试吧。': 'Connected. Send your first task to the bot in the Feishu group.',
|
|
331
|
+
}
|
|
332
|
+
if (exact[line]) return exact[line]
|
|
333
|
+
let match
|
|
334
|
+
if ((match = /^新群默认:(.*) · (.*?)。接入后可在控制台修改。$/.exec(line))) return `New group defaults: ${match[1]} · ${match[2]}. You can change them in the console after setup.`
|
|
335
|
+
if ((match = /^二维码 (\d+) 秒内有效。$/.exec(line))) return `The QR code expires in ${match[1]} seconds.`
|
|
336
|
+
if ((match = /^应用 ID:(.*)$/.exec(line))) return `App ID: ${match[1]}`
|
|
337
|
+
if ((match = /^项目目录 (.*) 就绪。$/.exec(line))) return `Project folder ready: ${match[1]}`
|
|
338
|
+
if ((match = /^配置已写入 (.*)。$/.exec(line))) return `Configuration saved to ${match[1]}.`
|
|
339
|
+
if ((match = /^目录 (.*) 已存在且非空。/.exec(line))) return `The folder ${match[1]} already contains files. Choose another project name, or enter that folder in the existing-project field.`
|
|
340
|
+
if (/^目录 /.test(line)) return 'This folder is too broad to use as a project. Choose a specific project folder.'
|
|
341
|
+
const codes = [...line.matchAll(/(?:code[ ::]+|HTTP\s+|()(\d{3,})/g)].map(m => m[1])
|
|
342
|
+
const suffix = codes.length ? ` (code ${[...new Set(codes)].join(', ')})` : ''
|
|
343
|
+
const detail = /(([^)]+))/.exec(line)?.[1]
|
|
344
|
+
const extra = detail && !/[\u3400-\u9fff]/.test(detail) ? ` (${detail})` : suffix
|
|
345
|
+
if (/^二维码页面未能打开/.test(line)) return 'The separate QR window could not open. Use the QR code on this page.'
|
|
346
|
+
if (/^获取 tenant_access_token 失败/.test(line)) return `Could not obtain an app access token${extra}. Check that the app is available, then retry.`
|
|
347
|
+
if (/^查询应用状态失败/.test(line)) return `Could not read the app status${extra}. Retry after checking the app in the Feishu developer console.`
|
|
348
|
+
if (/^审核超时/.test(line)) return 'Approval is still pending. Retry after your organization administrator approves the app.'
|
|
349
|
+
if (/^自检未通过|^自检失败/.test(line)) return `Readiness checks failed${extra}. Review the bot, event subscriptions and permissions in the Feishu developer console, then retry.`
|
|
350
|
+
if (/^自检有提醒/.test(line)) return `Readiness checks reported warnings${extra}. Setup can continue, but some bot features may be unavailable. Review permissions and event subscriptions in the Feishu developer console.`
|
|
351
|
+
if (/^权限开通失败/.test(line)) return `Could not enable permissions${extra}. Setup can continue; ask an administrator to enable messaging and card permissions in the Feishu developer console.`
|
|
352
|
+
if (/^发布失败(210304)/.test(line)) return 'Publication failed (210304). Enable the bot as the default app ability in the Feishu developer console, then create and publish a version. Until publication, only the developer can use it.'
|
|
353
|
+
if (/^发布失败/.test(line)) return `Publication failed${extra}. Setup can continue. Publish a version in the Feishu developer console; until then, only the developer can use the bot.`
|
|
354
|
+
if (/^应用可用范围不含你|^自动建群失败/.test(line)) return `Could not create the group${extra}. Create a Feishu group, add the bot, then enter its group ID here to continue.`
|
|
355
|
+
if (/^创建项目目录失败/.test(line)) return `Could not create the project folder${extra}. Check folder permissions and retry.`
|
|
356
|
+
if (/^写入 .* 失败/.test(line)) return `Could not save the configuration${extra}. Check folder permissions and retry.`
|
|
357
|
+
if (/^初始化失败/.test(line)) return `Initialization failed${extra}. Your configuration is saved. Choose Retry initialization.`
|
|
358
|
+
if (/[\u3400-\u9fff]/.test(line)) return `Setup could not complete this step${suffix}. Check the app in the Feishu developer console and retry.`
|
|
359
|
+
return line
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function setupPage({ version = '' } = {}) {
|
|
363
|
+
return String.raw`<!doctype html>
|
|
364
|
+
<html lang="en" data-sintbot-setup="true"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
365
|
+
<title>SintBot · Connect Feishu</title>
|
|
366
|
+
<style>
|
|
367
|
+
:root{--ink:#13171c;--muted:#6b7280;--line:#e5e7eb;--paper:#f6f5f1;--copper:#dd7d3d;--fail:#b91c1c;--ok:#15803d}
|
|
368
|
+
*{box-sizing:border-box}body{margin:0;background:var(--paper);color:var(--ink);font:15px/1.6 -apple-system,"PingFang SC","Helvetica Neue",sans-serif}
|
|
369
|
+
.wrap{max-width:720px;margin:0 auto;padding:40px 20px 60px}h1{font-size:24px;margin:0 0 6px}.sub{color:var(--muted);margin:0 0 26px}
|
|
370
|
+
.card{background:#fff;border:1px solid var(--line);border-radius:12px;padding:22px 24px;margin-bottom:18px}
|
|
371
|
+
label.f{display:block;font-size:13px;font-weight:600;margin:14px 0 6px}input[type=text]{width:100%;padding:10px 12px;border:1px solid var(--line);border-radius:8px;font:inherit}
|
|
372
|
+
.hint{color:var(--muted);font-size:13px;margin:6px 0 0}.row{display:flex;gap:16px;flex-wrap:wrap;align-items:center}
|
|
373
|
+
.btn{display:inline-block;padding:10px 18px;border-radius:8px;border:1px solid var(--line);background:#fff;font:inherit;cursor:pointer;min-height:40px}
|
|
374
|
+
.btn.primary{background:var(--copper);border-color:var(--copper);color:#fff}.btn:disabled{opacity:.5;cursor:default}
|
|
375
|
+
.qr{display:flex;justify-content:center;padding:10px 0}.qr svg{width:min(300px,100%);height:auto;aspect-ratio:1}
|
|
376
|
+
.log{font-size:13px;line-height:1.7;max-height:320px;overflow:auto;background:var(--paper);border-radius:8px;padding:12px 14px;white-space:pre-wrap;word-break:break-word}
|
|
377
|
+
.status{font-weight:600}.status.failed{color:var(--fail)}.status.done{color:var(--ok)}
|
|
378
|
+
.steps{margin:0;padding-left:20px;color:var(--muted);font-size:13px}.hidden{display:none}
|
|
379
|
+
</style></head><body><div class="wrap">
|
|
380
|
+
<div class="row" style="justify-content:space-between"><h1 data-i18n="title">Connect your project to Feishu</h1><label class="hint">Language <select id="language" aria-label="Language"><option value="en">English</option><option value="zh">简体中文</option></select></label></div><p class="sub">SintBot ${String(version).replace(/[&<>"\']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c])} · <span data-i18n="intro">This page is only available on this Mac. After you confirm the scan, setup creates the app, project folder and group, then saves your configuration.</span></p>
|
|
381
|
+
|
|
382
|
+
<div class="card" id="form">
|
|
383
|
+
<ol class="steps"><li data-i18n="step1">Name a new project, or choose an existing folder</li><li data-i18n="step2">Scan with Feishu on your phone and confirm (a company or team account is required)</li><li data-i18n="step3">Wait for setup to create the app and group, then send your first task in the new group</li></ol>
|
|
384
|
+
<label class="f" for="name" data-i18n="name">Project name</label><input type="text" id="name" value="sintbot" autocomplete="off">
|
|
385
|
+
<div class="hint"><span data-i18n="rootBefore">A new folder with a README will be created under</span> <span id="root"></span><span data-i18n="rootAfter">.</span></div>
|
|
386
|
+
<label class="f" for="workdir" data-i18n="workdir">Or use an existing project (absolute path; leave blank to create one)</label><input type="text" id="workdir" placeholder="/Users/you/Projects/my-app" autocomplete="off">
|
|
387
|
+
<label class="f" data-i18n="executor">Executor</label>
|
|
388
|
+
<div class="row" id="providers"></div>
|
|
389
|
+
<div class="hint"><span data-i18n="executorHint">Choose if both are installed. A single available executor is selected automatically.</span></div>
|
|
390
|
+
<div class="hint" data-i18n="modelHint">Before scanning, SintBot reads the executor’s live recommended model and thinking depth and saves them for the new group. You can change them in the console after setup.</div>
|
|
391
|
+
<label class="f"><input type="checkbox" id="basic"> <span data-i18n="basic">I am not an organization administrator (basic setup)</span></label>
|
|
392
|
+
<div class="hint"><span data-i18n="basicHint">Skip availability changes and publication. Ask an administrator to finish these steps in the Feishu developer console.</span></div>
|
|
393
|
+
<div class="row" style="margin-top:18px"><button class="btn primary" id="start" disabled data-i18n="start">Start scan</button><span class="hint" id="formErr"></span></div>
|
|
394
|
+
</div>
|
|
395
|
+
|
|
396
|
+
<div class="card hidden" id="run">
|
|
397
|
+
<div class="row" style="justify-content:space-between"><span class="status" id="status">Preparing…</span><button class="btn" id="cancel" data-i18n="cancel">Cancel</button></div>
|
|
398
|
+
<div class="hint hidden" id="modelDefaults" style="margin:10px 0;overflow-wrap:anywhere"></div>
|
|
399
|
+
<div class="qr" id="qr"></div>
|
|
400
|
+
<div class="log" id="log"></div>
|
|
401
|
+
<div id="chat" class="hidden">
|
|
402
|
+
<div class="hint" style="margin-top:12px"><span data-i18n="chatHint">The group could not be created automatically. Create a Feishu group, add the bot, then paste its group ID (oc_…) here:</span></div>
|
|
403
|
+
<div class="row" style="margin-top:8px"><input type="text" id="chatId" placeholder="oc_…" style="flex:1;min-width:0;width:100%"><button class="btn primary" id="resume" data-i18n="resume">Continue</button></div>
|
|
404
|
+
</div>
|
|
405
|
+
<div id="retry" class="hidden"><div class="row" style="margin-top:12px"><button class="btn" id="back" data-i18n="back">Edit details</button><button class="btn primary" id="again" data-i18n="again">Retry</button></div></div>
|
|
406
|
+
<div id="chatBack" class="hidden"><div class="row" style="margin-top:8px"><button class="btn" id="back2" data-i18n="back2">Back to project details</button></div></div>
|
|
407
|
+
<div id="doneBox" class="hidden"><div class="hint" style="margin-top:12px"><span data-i18n="doneHint">Your configuration is saved. SintBot is connecting to Feishu and this page will open the console. Try asking the bot in the new group to read the first line of README.</span></div></div>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
<script>
|
|
411
|
+
(function(){
|
|
412
|
+
const $=s=>document.querySelector(s);
|
|
413
|
+
const translate=${translateSetupNotice.toString()};
|
|
414
|
+
const words={en:{modelHint:'Before scanning, SintBot reads the executor’s live recommended model and thinking depth and saves them for the new group. You can change them in the console after setup.',modelDefaults:'New group defaults',thinking:'thinking depth',title:'Connect your project to Feishu',intro:'This page is only available on this Mac. After you confirm the scan, setup creates the app, project folder and group, then saves your configuration.',step1:'Name a new project, or choose an existing folder',step2:'Scan with Feishu on your phone and confirm (a company or team account is required)',step3:'Wait for setup to create the app and group, then send your first task in the new group',name:'Project name',rootBefore:'A new folder with a README will be created under',rootAfter:'.',workdir:'Or use an existing project (absolute path; leave blank to create one)',executor:'Executor',executorHint:'Choose if both are installed. A single available executor is selected automatically.',basic:'I am not an organization administrator (basic setup)',basicHint:'Skip availability changes and publication. Ask an administrator to finish these steps in the Feishu developer console.',start:'Start scan',cancel:'Cancel',chatHint:'The group could not be created automatically. Create a Feishu group, add the bot, then paste its group ID (oc_…) here:',resume:'Continue',back:'Edit details',again:'Retry',back2:'Back to project details',doneHint:'Your configuration is saved. SintBot is connecting to Feishu and this page will open the console. Try asking the bot in the new group to read the first line of README.',noRunner:'No available executor was detected. Install Codex or Claude Code and sign in, then reopen this page.',waiting:'Waiting for scan confirmation…',preparing:'Preparing…',chat:'Create a group to continue',done:'Connected',failed:'Setup did not complete',init:'Retry initialization',launching:'Connected. Starting the console…',switching:'Opening the console…',cannotCancel:'This step cannot be cancelled yet. Please wait.',cancelling:'Cancelling at the next step boundary…',offline:'Cannot reach setup. Check that SintBot is still running, then reload this page.',handoffFailed:'The console did not become ready. Check the terminal for a startup error, then reload this page.',retryConnection:'Reload page',requestFailed:'The request failed. Please retry.'},
|
|
415
|
+
zh:{modelHint:'扫码前读取执行器实时推荐的型号和思考深度,并保存为新群的初始设置;接入后可在控制台修改。',modelDefaults:'新群默认',thinking:'思考深度',title:'把项目接进飞书',intro:'这一页只在这台 Mac 上可见。扫码确认后会自动创建机器人应用、项目目录和群,并写好配置。',step1:'填项目名,或选一个已有目录',step2:'用手机飞书扫码并确认(需要企业或团队身份)',step3:'等它自动建应用、建群、写配置,然后在新群里发第一条任务',name:'项目名',rootBefore:'会在',rootAfter:'下新建这个目录并放一份 README。',workdir:'或者接入已有项目(绝对路径,留空则按上面新建)',executor:'执行器',executorHint:'两个都装了才需要选;只装了一个会自动用它。',basic:'我不是组织管理员(精简接入)',basicHint:'跳过放开范围和发布版本,之后请管理员在开放平台补这两步。',start:'开始扫码',cancel:'取消',chatHint:'自动建群没有成功。请在飞书里手动建一个群,把机器人拉进去,复制群 ID(oc_…)粘贴到这里继续:',resume:'继续',back:'返回修改',again:'重试',back2:'返回项目设置',doneHint:'配置已写好,SintBot 正在连接飞书。这一页几秒后会切到控制台;去新群里发「读取 README 第一行」试试。',noRunner:'未检测到可用执行器,请先安装 Codex 或 Claude Code 并登录,再重新打开此页面。',waiting:'等待扫码确认…',preparing:'正在准备…',chat:'需要你手动建群',done:'接入完成',failed:'接入没有完成',init:'重试初始化',launching:'接入完成,正在启动控制台…',switching:'正在切换到控制台…',cannotCancel:'现在这一步不能取消,请稍等。',cancelling:'正在取消,等待当前步骤结束…',offline:'暂时无法连接接入页。请确认 SintBot 仍在运行,然后刷新页面。',handoffFailed:'控制台未能就绪,请查看终端中的启动错误后刷新页面。',retryConnection:'刷新页面',requestFailed:'请求失败,请重试。'}};
|
|
416
|
+
let lang='en';try{const saved=localStorage.getItem('sbot-lang');lang=saved==='zh'||saved==='en'?saved:(navigator.language||'').toLowerCase().startsWith('zh')?'zh':'en'}catch(e){lang=(navigator.language||'').toLowerCase().startsWith('zh')?'zh':'en'}
|
|
417
|
+
const t=k=>words[lang][k]||k;
|
|
418
|
+
const api=async(p,b)=>{const r=await fetch(p,b?{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(b),signal:AbortSignal.timeout(5000)}:{cache:'no-store',signal:AbortSignal.timeout(5000)});const j=await r.json().catch(()=>({}));if(!r.ok)throw new Error(j.error||String(r.status));return j};
|
|
419
|
+
let last=null,timer=null,formError='',actionError='',actionKey='',polling=false,handoffStarted=false;
|
|
420
|
+
function displayError(error){return translate(error&&error.message||String(error),lang)||t('requestFailed')}
|
|
421
|
+
function renderForm(st){$('#root').textContent=st.defaults.projectsRoot;const box=$('#providers');const chosen=document.querySelector('input[name=prov]:checked')?.value;box.replaceChildren();const avail=Object.keys(st.runners||{}).filter(k=>st.runners[k]);$('#start').disabled=!avail.length;
|
|
422
|
+
if(!avail.length){const hint=document.createElement('span');hint.className='hint';hint.style.color='var(--fail)';hint.textContent=t('noRunner');box.appendChild(hint);return}
|
|
423
|
+
if(avail.length===1){box.textContent=avail[0]==='claude'?'Claude Code':'Codex';return}
|
|
424
|
+
for(const r of avail){const l=document.createElement('label'),radio=document.createElement('input');radio.type='radio';radio.name='prov';radio.value=r;radio.checked=r===(chosen||avail[0]);l.append(radio,document.createTextNode(' '+(r==='claude'?'Claude Code':'Codex')));box.appendChild(l)}}
|
|
425
|
+
function restore(st){if(!st.input)return;$('#name').value=st.input.name||'sintbot';$('#workdir').value=st.input.workdir||'';$('#basic').checked=!!st.input.basic;if(st.input.provider){const r=document.querySelector('input[name=prov][value="'+st.input.provider+'"]');if(r)r.checked=true}}
|
|
426
|
+
function render(st){last=st;if(st.phase!=='running')actionKey='';const running=st.phase!=='form';$('#form').classList.toggle('hidden',running);$('#run').classList.toggle('hidden',!running);$('#formErr').textContent=formError?translate(formError,lang):'';if(!running)return;
|
|
427
|
+
$('#status').textContent=t(st.phase==='running'?(st.qr?'waiting':'preparing'):st.phase==='waitingChat'?'chat':st.phase==='done'?'done':'failed');
|
|
428
|
+
$('#modelDefaults').classList.toggle('hidden',!st.modelDefaults);$('#modelDefaults').textContent=st.modelDefaults?t('modelDefaults')+': '+(st.modelDefaults.name||st.modelDefaults.model)+' · '+t('thinking')+': '+st.modelDefaults.effort:'';
|
|
429
|
+
$('#status').className='status '+st.phase;$('#qr').innerHTML=st.phase==='running'&&st.qr?st.qr:'';$('#log').textContent=st.lines.map(l=>translate(l,lang)).concat(actionKey?[t(actionKey)]:actionError?[translate(actionError,lang)]:[]).join('\n');$('#log').scrollTop=1e9;
|
|
430
|
+
$('#cancel').classList.toggle('hidden',st.phase!=='running');$('#chat').classList.toggle('hidden',st.phase!=='waitingChat');$('#chatBack').classList.toggle('hidden',st.phase!=='waitingChat');$('#retry').classList.toggle('hidden',st.phase!=='failed');$('#doneBox').classList.toggle('hidden',st.phase!=='done');$('#again').textContent=t(st.initOnly?'init':'again')}
|
|
431
|
+
function setLanguage(value){lang=value==='zh'?'zh':'en';$('#language').value=lang;document.documentElement.lang=lang==='zh'?'zh-CN':'en';document.title='SintBot · '+(lang==='zh'?'接入飞书':'Connect Feishu');document.querySelectorAll('[data-i18n]').forEach(el=>{el.textContent=t(el.dataset.i18n)});if(last){renderForm(last);render(last)}try{localStorage.setItem('sbot-lang',lang)}catch(e){}}
|
|
432
|
+
$('#language').onchange=()=>setLanguage($('#language').value);setLanguage(lang);
|
|
433
|
+
// A valid console response must be HTML without the setup marker. A failed
|
|
434
|
+
// request can mean normal handoff or a stopped process; the wait is bounded.
|
|
435
|
+
async function handoff(){if(handoffStarted)return;handoffStarted=true;clearInterval(timer);$('#status').textContent=t(last?.phase==='done'?'launching':'switching');for(let i=0;i<60;i++){try{const r=await fetch('/',{cache:'no-store',signal:AbortSignal.timeout(3000)});if(r.ok&&/text\/html/.test(r.headers.get('content-type')||'')){const html=await r.text();if(!html.includes('data-sintbot-setup=')&&html.includes('<html')){location.replace('/');return}}}catch(e){}await new Promise(r=>setTimeout(r,2000))}actionError=t('handoffFailed');$('#status').textContent=t('failed');$('#log').textContent+='\n'+actionError;const reload=document.createElement('button');reload.className='btn';reload.textContent=t('retryConnection');reload.onclick=()=>location.reload();$('#run').appendChild(reload)}
|
|
436
|
+
let gone=0;async function poll(){if(polling||handoffStarted)return;polling=true;try{const st=await api('/api/setup/state');if(!st.phase){await handoff();return}gone=0;if(!last){renderForm(st);restore(st)}formError='';render(st);if(st.phase==='done')void handoff()}catch(e){if(last&&last.phase!=='form'&&++gone>=2)void handoff();else if(!last||last.phase==='form'){formError=t('offline');$('#formErr').textContent=formError}}finally{polling=false}}
|
|
437
|
+
async function start(extra){formError='';actionError='';actionKey='';$('#formErr').textContent='';const prov=document.querySelector('input[name=prov]:checked');const input={name:$('#name').value.trim(),workdir:$('#workdir').value.trim(),provider:prov?prov.value:'',basic:$('#basic').checked,...extra};
|
|
438
|
+
try{const r=await api('/api/setup/start',input);render(r.state)}catch(e){formError=e.message;actionError=e.message;if(last)render(last);else $('#formErr').textContent=displayError(e)}}
|
|
439
|
+
$('#start').onclick=()=>start({});$('#again').onclick=()=>start({});$('#resume').onclick=()=>start({chatId:$('#chatId').value.trim()});
|
|
440
|
+
$('#cancel').onclick=async()=>{try{const r=await api('/api/setup/cancel',{});actionKey=r.ok?'cancelling':'cannotCancel';render(last)}catch(e){actionError=e.message;render(last)}};
|
|
441
|
+
const back=async()=>{try{await api('/api/setup/reset',{});formError='';actionError='';actionKey='';render(await api('/api/setup/state'))}catch(e){actionError=e.message;render(last)}};$('#back').onclick=back;$('#back2').onclick=back;
|
|
442
|
+
api('/api/setup/state').then(st=>{renderForm(st);restore(st);render(st)}).catch(e=>{formError=t('offline');$('#formErr').textContent=formError});timer=setInterval(poll,1500);
|
|
443
|
+
})();
|
|
444
|
+
</script></body></html>`
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
module.exports = { createSetupServer, createSetupSession, probeRunners, setupPage, svgOf, translateSetupNotice }
|
package/src/ui/console.html
CHANGED
|
@@ -3651,7 +3651,7 @@ function renderSettings(){
|
|
|
3651
3651
|
${(pri.dropped||[]).map(d=>`<div class="hint" style="color:var(--fail)">${IC.x} 配置里已忽略 ${esc(d.value)}:${esc(d.why)}</div>`).join('')}
|
|
3652
3652
|
${(pri.outside||[]).length?`<div class="hint" style="margin-top:6px">${IC.warn} ${pri.outside.length} 个群在根之外:${esc(pri.outside.map(o=>o.name||o.chatId).join('、'))} —— 照常运行,改目录时须搬进根内</div>`:''}
|
|
3653
3653
|
</div>
|
|
3654
|
-
<div class="hint" style="margin-top:14px;padding-top:12px;border-top:1px solid var(--line);line-height:1.7">这里只读,控制台不提供修改。要改,在服务器上编辑配置文件里的 <code>projectsRoots</code>(一个路径或数组,认 ~)
|
|
3654
|
+
<div class="hint" style="margin-top:14px;padding-top:12px;border-top:1px solid var(--line);line-height:1.7">这里只读,控制台不提供修改。要改,在服务器上编辑配置文件里的 <code>projectsRoots</code>(一个路径或数组,认 ~);改后即时生效,不用重启。</div>`;
|
|
3655
3655
|
const urlRow=(label,u)=>`<div class="kv"><span class="kk">${label}</span><span class="vv mono"><a href="${esc(u)}" target="_blank" style="color:var(--ember-soft)">${esc(u)}</a></span></div>`;
|
|
3656
3656
|
// 「服务器」卡 = 访问地址 + 项目根目录(用户 2026-09-08:两张短卡并成一张,不留空当;本机 127.0.0.1 那行去掉 ——
|
|
3657
3657
|
// 正在看控制台的人用不着它,而它又是三行里最像「可以点」却最没用的一行)
|
|
@@ -3697,7 +3697,7 @@ function renderSettings(){
|
|
|
3697
3697
|
${px&&px.noProxy?`<div class="hint">直连:<span class="mono">${esc(px.noProxy)}</span></div>`:''}
|
|
3698
3698
|
</details>`:''}
|
|
3699
3699
|
<div class="botnet-actions"><button class="btn primary" id="bSave">保存</button>
|
|
3700
|
-
<div class="hint"
|
|
3700
|
+
<div class="hint">代理第一次填,新的对话轮次就生效;改已有的地址要重启 SintBot(按 Ctrl-C 退出后用原来的启动命令重新启动)。</div></div>
|
|
3701
3701
|
</div>`;
|
|
3702
3702
|
const updateJob=upd&&upd.job;
|
|
3703
3703
|
// 1.4.x 多群服务的升级器带 mode:后台托管才能一键升级,前台与开发版只给说明。版本变化后的群播报由启动时的公告卡片负责(控制台可关)。
|
|
@@ -3967,6 +3967,8 @@ function bindWecomCard(){
|
|
|
3967
3967
|
}
|
|
3968
3968
|
// The connection view is read-only; onboarding is completed on the host Mac.
|
|
3969
3969
|
// Public console actions are limited to group management and access administration.
|
|
3970
|
+
// Guide links follow the console language (the site keeps English at / and Chinese at /zh/).
|
|
3971
|
+
const guideBase=()=>document.documentElement.lang==='zh'?'https://sintbot.com/zh/guide/':'https://sintbot.com/guide/';
|
|
3970
3972
|
function fxFeishuCard(fe){
|
|
3971
3973
|
const configured=!!(fe&&(fe.appId||fe.appIdRef)&&fe.appSecretStatus&&fe.appSecretStatus!=='none');
|
|
3972
3974
|
const c=connDict(S.conn).feishu,online=!!(c&&c.state==='connected');
|
|
@@ -3979,12 +3981,12 @@ function fxFeishuCard(fe){
|
|
|
3979
3981
|
<p class="hint">${configured?'在项目群里发任务,机器人会把进度和结果回复到群里。':'用手机飞书确认一次,自动创建机器人和项目群。'}</p>
|
|
3980
3982
|
${configured?`<div class="feishu-current"><span>已绑定项目群</span><strong>${groupCount}</strong><span>消息响应</span><strong>${fe.onlyWhenMentioned?'仅回复 @ 机器人的消息':'接收群内普通消息'}</strong></div>
|
|
3981
3983
|
${!online?'<p class="hint">应用已配置,连接尚未确认。请确认这台 Mac 上的 SintBot 正在运行。</p>':''}
|
|
3982
|
-
<div class="feishu-actions"><button class="btn primary" data-gotab="groups">管理项目群</button><a class="btn" href="
|
|
3984
|
+
<div class="feishu-actions"><button class="btn primary" data-gotab="groups">管理项目群</button><a class="btn" data-guide="usage/" href="${guideBase()}usage/" target="_blank" rel="noopener">查看使用指南</a></div>`:''}
|
|
3983
3985
|
<details class="feishu-details" ${configured?'':'open'}><summary>首次接入:三步开始</summary><ol class="feishu-steps">
|
|
3984
3986
|
<li><b>手机扫码</b><span>按安装指南在 Mac 上开始接入,用手机飞书选择企业或团队身份,确认授权。</span></li>
|
|
3985
3987
|
<li><b>自动建群</b><span>接入流程创建机器人应用、项目目录和群,把你加入群并保存配置。</span></li>
|
|
3986
3988
|
<li><b>群里发任务</b><span>按提示启动后,在新群发「读取 README 第一行」。收到回复就可以开始工作。</span></li>
|
|
3987
|
-
</ol><div class="feishu-actions"><a class="btn ${configured?'':'primary'}" href="
|
|
3989
|
+
</ol><div class="feishu-actions"><a class="btn ${configured?'':'primary'}" data-guide="install/#feishu" href="${guideBase()}install/#feishu" target="_blank" rel="noopener">查看扫码接入指南</a></div>
|
|
3988
3990
|
<p class="hint">已经接入的应用可以继续使用,添加项目群请到「群 · 项目」。</p></details>
|
|
3989
3991
|
<details class="feishu-details"><summary>接入遇到问题</summary><dl class="feishu-help">
|
|
3990
3992
|
<dt>没有企业或团队身份</dt><dd>先在飞书创建团队或切换到已有团队,再开始扫码。</dd>
|
|
@@ -4000,7 +4002,7 @@ function fxFeishuCard(fe){
|
|
|
4000
4002
|
<dt>Bot ID</dt><dd class="mono">${esc(fe&&fe.botOpenId||'尚未获取')}</dd></dl>
|
|
4001
4003
|
<p class="hint">更换应用或凭据需要在这台 Mac 上完成,并在所有任务结束后重新启动。请保留已有配置和任务记录。</p>
|
|
4002
4004
|
<p class="hint">手动接入需配置机器人、消息事件和卡片回调、必要权限与可用范围,再发布应用版本。</p>
|
|
4003
|
-
<a class="lnk" href="
|
|
4005
|
+
<a class="lnk" data-guide="install/#feishu" href="${guideBase()}install/#feishu" target="_blank" rel="noopener">查看完整安装指南 ${IC.ext}</a>
|
|
4004
4006
|
</details>
|
|
4005
4007
|
</div>`;
|
|
4006
4008
|
}
|
|
@@ -4556,7 +4558,7 @@ const I18N_EN=[
|
|
|
4556
4558
|
["连不上 Claude / ChatGPT 时填 · 国内模型端点自动直连 · 留空 = 不用代理","Set this if Claude / ChatGPT can't be reached · China model endpoints connect directly · empty = no proxy"],
|
|
4557
4559
|
["后台服务的环境里已有代理:","The background service already has a proxy in its environment: "],
|
|
4558
4560
|
["直连:","Direct: "],
|
|
4559
|
-
["
|
|
4561
|
+
["代理第一次填,新的对话轮次就生效;改已有的地址要重启 SintBot(按 Ctrl-C 退出后用原来的启动命令重新启动)。","A first-time proxy applies to new turns right away; changing an existing address needs a SintBot restart (Ctrl-C, then the same start command)."],
|
|
4560
4562
|
["代理地址要像 http://127.0.0.1:7890","The proxy address should look like http://127.0.0.1:7890"],
|
|
4561
4563
|
["已接上聊天平台 —— 后台服务几秒后自动重启并连上","Chat platform connected — the background service restarts in a few seconds and connects"],
|
|
4562
4564
|
["已接上聊天平台 —— 在终端里重启 sintbot start --ui 连上","Chat platform connected — restart sintbot start --ui in the terminal to connect"],
|
|
@@ -5268,7 +5270,7 @@ const I18N_EN=[
|
|
|
5268
5270
|
["群项目都放这里 · 选目录、保存群绑定、从本机目录提炼风格,只认这些目录之内的路径 · 当前是默认值","All group projects live here · choosing a folder, saving a group binding, and extracting a style from a local folder only accept paths inside these folders · currently the default"],
|
|
5269
5271
|
["不存在 —— 在服务器上建好即可","Does not exist — just create it on the server"],
|
|
5270
5272
|
["这里只读,控制台不提供修改。要改,在服务器上编辑配置文件里的","Read-only here — the console offers no way to change it. To change it, edit "],
|
|
5271
|
-
["(一个路径或数组,认 ~)
|
|
5273
|
+
["(一个路径或数组,认 ~);改后即时生效,不用重启。"," in the config file on the server (one path, or an array; ~ is accepted); changes apply immediately, no restart needed."],
|
|
5272
5274
|
[";改后即时生效,不用重启。","; changes apply immediately, no restart needed."],
|
|
5273
5275
|
["公网","Public"],
|
|
5274
5276
|
["局域网","LAN"],
|
|
@@ -6096,6 +6098,7 @@ function setLang(l){
|
|
|
6096
6098
|
l=(l==='en')?'en':'zh';
|
|
6097
6099
|
try{localStorage.setItem('sbot-lang',l);}catch(e){}
|
|
6098
6100
|
const d=document.documentElement;d.dataset.lang=l;d.lang=l;
|
|
6101
|
+
for(const a of document.querySelectorAll('#feishuCard a[data-guide]'))a.href=guideBase()+a.dataset.guide;
|
|
6099
6102
|
document.title=l==='zh'?I18N_TITLE_ZH:trText(I18N_TITLE_ZH);
|
|
6100
6103
|
applyI18n(document.body);updateLangBtn();
|
|
6101
6104
|
if($('#verhistBox'))loadChangelog(); // 版本历史正文来自数据文件、不经词典 ⇒ 换语言得重画那张卡
|