@mhfire/dsh-im-bridge 0.1.2 → 0.1.3
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/README.en.md +82 -0
- package/README.md +33 -5
- package/cordis.patch.yml +44 -3
- package/lib/client.js +2 -0
- package/package.json +3 -2
- package/src/index.js +30 -5
- package/src/wecom.js +46 -26
package/README.en.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
[中文](./README.md) | English
|
|
2
|
+
|
|
3
|
+
# @mhfire/dsh-im-bridge
|
|
4
|
+
|
|
5
|
+
WeCom AI Bot ⇄ DeepSeek Harness Agent bridge — a **DSH plugin**.
|
|
6
|
+
|
|
7
|
+
Creates Agents **in-process** inside a dsh profile (no child-process spawn): per-sender durable sessions (the same WeCom user reuses one session with memory), sessions registered with the Web GUI (live view and continue-chat), plus a Settings → Plugins card (`allowFrom` / `agentTimeoutSec` / `startHint`, written to `settings.yaml` with hot reload).
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
### Recommended: npm package
|
|
12
|
+
|
|
13
|
+
```powershell
|
|
14
|
+
dsh plugin --profile web add @mhfire/dsh-im-bridge
|
|
15
|
+
# Or pin a version:
|
|
16
|
+
# dsh plugin --profile web add @mhfire/dsh-im-bridge@0.1.2
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
In `$DSH_HOME/profiles/web/cordis.patch.yml` (or your profile), supply credentials only (other fields ship as bundle defaults and can be overridden):
|
|
20
|
+
|
|
21
|
+
```yaml
|
|
22
|
+
- id: im-bridge
|
|
23
|
+
config:
|
|
24
|
+
botId: "<your BotID>"
|
|
25
|
+
secret: "<your Secret>"
|
|
26
|
+
# optional: workspace / personaFile / … — see Configuration below
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Restart dsh to apply (e.g. `dsh web` / `pnpm dsh web`).
|
|
30
|
+
|
|
31
|
+
### Optional: local development
|
|
32
|
+
|
|
33
|
+
Install from this repo’s `plugin/` directory or a `file:` path:
|
|
34
|
+
|
|
35
|
+
```powershell
|
|
36
|
+
dsh plugin --profile web add <package-path>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
If `botId` / `secret` are missing, the plugin still loads (does not block `dsh web`); logs warn that WeCom connect is skipped. You can fill credentials in Settings → Plugins; **restart** is required to connect (this release does not hot-start the WebSocket).
|
|
40
|
+
|
|
41
|
+
## Configuration
|
|
42
|
+
|
|
43
|
+
The bundle `cordis.patch.yml` supplies defaults for every field except `botId` / `secret`. Full field list:
|
|
44
|
+
|
|
45
|
+
| Field | Description |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `botId` / `secret` | WeCom AI Bot credentials (`role('secret')`, redacted in UI); when empty, WeCom side is skipped and the host keeps running |
|
|
48
|
+
| `workspace` | Agent working directory (session cwd) |
|
|
49
|
+
| `allowFrom` | Allowed sender userids; empty = allow everyone |
|
|
50
|
+
| `agentTimeoutSec` | Max seconds per task; also drives progress / ETA |
|
|
51
|
+
| `startHint` | Placeholder text when processing starts |
|
|
52
|
+
| `agentPreset` | Agent preset to mount (default `standard`) |
|
|
53
|
+
| `persona` / `personaFile` | Bot persona (system prompt); `personaFile` wins; do not commit secrets |
|
|
54
|
+
| `maxReplyBytes` | Reply size cap in bytes (default 20000) |
|
|
55
|
+
| `deniedMessage` | Reply when the sender is not on `allowFrom` (editable in Settings) |
|
|
56
|
+
| `welcomeMessage` | Welcome text on `enter_chat` (editable in Settings) |
|
|
57
|
+
| `thinking` | Streaming “thinking” animation: `phases` / `spin` / `eggs` / `eggAfterSec` / `intervalMs` / `activityPrefix`; override via profile patch (not the simple Settings card) |
|
|
58
|
+
|
|
59
|
+
Example `thinking.phases` override:
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
thinking:
|
|
63
|
+
intervalMs: 1500
|
|
64
|
+
phases:
|
|
65
|
+
- atSec: 0
|
|
66
|
+
text: '🤔 Understanding your request…'
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Persona
|
|
70
|
+
|
|
71
|
+
`persona.md` is the bot persona: role and behavior rules, injected as a system prompt each session.
|
|
72
|
+
Copy `persona.example.md` to `persona.md` and edit; supports `{{model}}` / `{{cwd}}`.
|
|
73
|
+
The file may contain credentials and is gitignored — do not commit it.
|
|
74
|
+
|
|
75
|
+
## Security
|
|
76
|
+
|
|
77
|
+
- Do not commit secret-bearing files such as `config.json` / `persona.md`
|
|
78
|
+
- Session and tool output may contain adversarial text; the plugin’s safety prompt tells the agent not to treat tool output as instructions
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT
|
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
中文 | [English](./README.en.md)
|
|
2
|
+
|
|
1
3
|
# @mhfire/dsh-im-bridge
|
|
2
4
|
|
|
3
5
|
企业微信智能机器人 ⇄ DeepSeek Harness Agent 桥接 **DSH 插件**。
|
|
@@ -6,27 +8,40 @@
|
|
|
6
8
|
|
|
7
9
|
## 安装
|
|
8
10
|
|
|
11
|
+
### 推荐:从 npm 安装
|
|
12
|
+
|
|
9
13
|
```powershell
|
|
10
|
-
dsh plugin --profile web add
|
|
14
|
+
dsh plugin --profile web add @mhfire/dsh-im-bridge
|
|
15
|
+
# 或钉版本:
|
|
16
|
+
# dsh plugin --profile web add @mhfire/dsh-im-bridge@0.1.2
|
|
11
17
|
```
|
|
12
18
|
|
|
13
|
-
在
|
|
19
|
+
在 `$DSH_HOME/profiles/web/cordis.patch.yml`(或对应 profile)中补密钥即可(其余项已有 bundle 默认,可按需覆盖):
|
|
14
20
|
|
|
15
21
|
```yaml
|
|
16
22
|
- id: im-bridge
|
|
17
23
|
config:
|
|
18
24
|
botId: "<你的 BotID>"
|
|
19
25
|
secret: "<你的 Secret>"
|
|
20
|
-
workspace
|
|
21
|
-
personaFile: "<绝对路径>/persona.md"
|
|
26
|
+
# 可选:workspace / personaFile 等,见下方配置项
|
|
22
27
|
```
|
|
23
28
|
|
|
24
|
-
重启 dsh
|
|
29
|
+
重启 dsh 进程即可使用(例如 `dsh web` / `pnpm dsh web`)。
|
|
30
|
+
|
|
31
|
+
### 备选:本地开发
|
|
32
|
+
|
|
33
|
+
从本仓库 `plugin/` 目录或 `file:` 路径安装:
|
|
34
|
+
|
|
35
|
+
```powershell
|
|
36
|
+
dsh plugin --profile web add <本包路径>
|
|
37
|
+
```
|
|
25
38
|
|
|
26
39
|
未配置 `botId` / `secret` 时插件仍会加载(不阻塞 `dsh web`),日志会提示跳过企微连线;Settings → 插件配置页仍可填写,保存后**重启**进程才会连接(本版本不做热启连)。
|
|
27
40
|
|
|
28
41
|
## 配置项
|
|
29
42
|
|
|
43
|
+
bundle 的 `cordis.patch.yml` 已为除 `botId` / `secret` 外的字段提供默认值;下表为完整说明。
|
|
44
|
+
|
|
30
45
|
| 字段 | 说明 |
|
|
31
46
|
|---|---|
|
|
32
47
|
| `botId` / `secret` | 企业微信智能机器人凭证(`role('secret')`,UI 自动脱敏);缺省时跳过企微侧,不阻塞主进程 |
|
|
@@ -37,6 +52,19 @@ dsh plugin --profile web add <本包路径>
|
|
|
37
52
|
| `agentPreset` | Agent 加入的 preset(默认 `standard`) |
|
|
38
53
|
| `persona` / `personaFile` | 机器人「人设」(系统提示词);`personaFile` 优先,含敏感信息请勿入库 |
|
|
39
54
|
| `maxReplyBytes` | 回复上限(字节,默认 20000) |
|
|
55
|
+
| `deniedMessage` | 非白名单用户的拒绝文案(Settings 可编) |
|
|
56
|
+
| `welcomeMessage` | 进入会话欢迎语(Settings 可编) |
|
|
57
|
+
| `thinking` | 流式思考动画:`phases` / `spin` / `eggs` / `eggAfterSec` / `intervalMs` / `activityPrefix`;结构请在 profile patch 覆盖,简易卡片不编辑 |
|
|
58
|
+
|
|
59
|
+
`thinking.phases` 示例(profile 覆盖时可只改文案):
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
thinking:
|
|
63
|
+
intervalMs: 1500
|
|
64
|
+
phases:
|
|
65
|
+
- atSec: 0
|
|
66
|
+
text: '🤔 正在理解你的需求…'
|
|
67
|
+
```
|
|
40
68
|
|
|
41
69
|
## 人设(persona)
|
|
42
70
|
|
package/cordis.patch.yml
CHANGED
|
@@ -1,6 +1,47 @@
|
|
|
1
|
-
# dsh-im-bridge bundle patch:
|
|
2
|
-
#
|
|
3
|
-
# 不要写在这里。
|
|
1
|
+
# dsh-im-bridge bundle patch: 插入插件行并给出非密钥默认配置。
|
|
2
|
+
# botId/secret 由 profile 层 ($DSH_HOME/profiles/<name>/cordis.patch.yml) 或 Settings 覆盖。
|
|
4
3
|
- insert:
|
|
5
4
|
- id: im-bridge
|
|
6
5
|
name: '@mhfire/dsh-im-bridge'
|
|
6
|
+
config:
|
|
7
|
+
workspace: !!js process.cwd()
|
|
8
|
+
allowFrom: []
|
|
9
|
+
startHint: '🧠 正在思考...'
|
|
10
|
+
agentTimeoutSec: 600
|
|
11
|
+
agentPreset: standard
|
|
12
|
+
persona: ''
|
|
13
|
+
personaFile: ''
|
|
14
|
+
maxReplyBytes: 20000
|
|
15
|
+
deniedMessage: '无权访问本服务'
|
|
16
|
+
welcomeMessage: '👋 办公助手已就绪。直接发消息即可,例如查文件、整理文档、查资料或处理日常事务。'
|
|
17
|
+
thinking:
|
|
18
|
+
eggAfterSec: 240
|
|
19
|
+
intervalMs: 1500
|
|
20
|
+
activityPrefix: '🛠️ 正在执行 '
|
|
21
|
+
spin:
|
|
22
|
+
- '🧠'
|
|
23
|
+
- '💭'
|
|
24
|
+
- '✨'
|
|
25
|
+
- '🔎'
|
|
26
|
+
- '⚡'
|
|
27
|
+
eggs:
|
|
28
|
+
- '📎 顺手把要点整理好了,稍后一起给你'
|
|
29
|
+
- '📶 网络有点忙,让它慢慢跑'
|
|
30
|
+
- '🎯 结果快出来了,坚持一下'
|
|
31
|
+
- '🗂️ 资料较多,正在汇总中'
|
|
32
|
+
- '🌙 别盯着了,完成会自动通知你'
|
|
33
|
+
phases:
|
|
34
|
+
- atSec: 0
|
|
35
|
+
text: '🤔 正在理解你的需求…'
|
|
36
|
+
- atSec: 8
|
|
37
|
+
text: '📋 正在整理任务清单'
|
|
38
|
+
- atSec: 25
|
|
39
|
+
text: '🔍 正在查找相关资料'
|
|
40
|
+
- atSec: 55
|
|
41
|
+
text: '✍️ 正在处理文档/数据'
|
|
42
|
+
- atSec: 120
|
|
43
|
+
text: '🧠 正在思考最佳方案…'
|
|
44
|
+
- atSec: 240
|
|
45
|
+
text: '⏳ 任务较繁琐,请稍候…'
|
|
46
|
+
- atSec: 420
|
|
47
|
+
text: '☕ 快好了,正在收尾…'
|
package/lib/client.js
CHANGED
|
@@ -18,6 +18,8 @@ var FIELDS = [
|
|
|
18
18
|
{ field: 'allowFrom', label: '允许的发送者 userid(逗号分隔,空 = 所有人)', kind: 'text' },
|
|
19
19
|
{ field: 'agentTimeoutSec', label: '单任务超时(秒)', kind: 'number' },
|
|
20
20
|
{ field: 'startHint', label: '开始处理时的占位提示', kind: 'text' },
|
|
21
|
+
{ field: 'deniedMessage', label: '非白名单拒绝文案', kind: 'text' },
|
|
22
|
+
{ field: 'welcomeMessage', label: '进入会话欢迎语', kind: 'text' },
|
|
21
23
|
];
|
|
22
24
|
|
|
23
25
|
function fmt(f, v) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mhfire/dsh-im-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "企业微信智能机器人 ⇄ DeepSeek Harness Agent 桥接插件:进程内创建 Agent(per-sender 持久会话),会话在 GUI 实时可见;含 Settings 插件配置卡片",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"src",
|
|
20
20
|
"lib",
|
|
21
21
|
"cordis.patch.yml",
|
|
22
|
-
"persona.example.md"
|
|
22
|
+
"persona.example.md",
|
|
23
|
+
"README.en.md"
|
|
23
24
|
],
|
|
24
25
|
"dsh": {
|
|
25
26
|
"bundle": {
|
package/src/index.js
CHANGED
|
@@ -19,7 +19,7 @@ import z from '@deepseek-ai/schemastery'
|
|
|
19
19
|
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
20
20
|
import { SessionId } from '@deepseek-ai/dsh-session'
|
|
21
21
|
import { installModelSelection } from '@deepseek-ai/dsh-agent'
|
|
22
|
-
import { startThinking, sendFinal, truncate, footerOf, fmtDuration } from './wecom.js'
|
|
22
|
+
import { startThinking, sendFinal, truncate, footerOf, fmtDuration, DEFAULT_THINKING } from './wecom.js'
|
|
23
23
|
|
|
24
24
|
/** 稳定插件名 */
|
|
25
25
|
export const name = 'im-bridge'
|
|
@@ -27,6 +27,20 @@ export const name = 'im-bridge'
|
|
|
27
27
|
/** 依赖的核心服务 */
|
|
28
28
|
export const inject = ['agents', 'sessions', 'agentDefaultModel']
|
|
29
29
|
|
|
30
|
+
const ThinkingPhase = z.object({
|
|
31
|
+
atSec: z.number(),
|
|
32
|
+
text: z.string(),
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const ThinkingConfig = z.object({
|
|
36
|
+
phases: z.array(ThinkingPhase).default(DEFAULT_THINKING.phases),
|
|
37
|
+
spin: z.array(String).default(DEFAULT_THINKING.spin),
|
|
38
|
+
eggs: z.array(String).default(DEFAULT_THINKING.eggs),
|
|
39
|
+
eggAfterSec: z.number().default(DEFAULT_THINKING.eggAfterSec),
|
|
40
|
+
intervalMs: z.number().default(DEFAULT_THINKING.intervalMs),
|
|
41
|
+
activityPrefix: z.string().default(DEFAULT_THINKING.activityPrefix),
|
|
42
|
+
})
|
|
43
|
+
|
|
30
44
|
/** 插件配置(zod): 敏感项由 profile 层 patch 或 Settings 提供;缺省时跳过企微连线,不阻塞主进程 */
|
|
31
45
|
export const Config = z.object({
|
|
32
46
|
botId: z.string().default('').role('secret'),
|
|
@@ -47,6 +61,12 @@ export const Config = z.object({
|
|
|
47
61
|
personaFile: z.string().default(''),
|
|
48
62
|
/** 回复上限(字节) */
|
|
49
63
|
maxReplyBytes: z.number().default(20000),
|
|
64
|
+
/** 流式思考动画素材(阶段/表情/彩蛋等); 可在 profile patch 覆盖 */
|
|
65
|
+
thinking: ThinkingConfig.default(DEFAULT_THINKING),
|
|
66
|
+
/** 非白名单用户的拒绝文案 */
|
|
67
|
+
deniedMessage: z.string().default('无权访问本服务'),
|
|
68
|
+
/** 用户进入会话时的欢迎语 */
|
|
69
|
+
welcomeMessage: z.string().default('👋 办公助手已就绪。直接发消息即可,例如查文件、整理文档、查资料或处理日常事务。'),
|
|
50
70
|
})
|
|
51
71
|
|
|
52
72
|
/** 收集一次 turn 内最后一条 assistant 文本与结束原因 */
|
|
@@ -158,8 +178,9 @@ export function apply(ctx, config) {
|
|
|
158
178
|
// 会话事件 → 真实活动状态(工具调用名), 让动画显示"正在做什么"
|
|
159
179
|
ctx.on('session/event', (session, event) => {
|
|
160
180
|
if (event.type !== 'tool/call') return
|
|
181
|
+
const prefix = cfg().thinking?.activityPrefix ?? DEFAULT_THINKING.activityPrefix
|
|
161
182
|
for (const st of senders.values()) {
|
|
162
|
-
if (st.sessionId === session.id) st.lastActivity =
|
|
183
|
+
if (st.sessionId === session.id) st.lastActivity = `${prefix}${event.data.name}`
|
|
163
184
|
}
|
|
164
185
|
})
|
|
165
186
|
|
|
@@ -173,7 +194,11 @@ export function apply(ctx, config) {
|
|
|
173
194
|
let stopThinking = null
|
|
174
195
|
try {
|
|
175
196
|
await ws.replyStream(frame, streamId, cfg().startHint, false)
|
|
176
|
-
stopThinking = startThinking(
|
|
197
|
+
stopThinking = startThinking(
|
|
198
|
+
ws, frame, streamId, startedAt, cfg().agentTimeoutSec,
|
|
199
|
+
() => st.lastActivity || '',
|
|
200
|
+
cfg().thinking,
|
|
201
|
+
)
|
|
177
202
|
} catch (e) {
|
|
178
203
|
console.error(`[im-bridge] 占位回复失败: ${e.message}`)
|
|
179
204
|
}
|
|
@@ -217,7 +242,7 @@ export function apply(ctx, config) {
|
|
|
217
242
|
if (!content) return
|
|
218
243
|
const sender = frame.body?.sender?.userid || frame.body?.from?.userid || frame.body?.userid || 'unknown'
|
|
219
244
|
if (cfg().allowFrom.length > 0 && !cfg().allowFrom.includes(sender)) {
|
|
220
|
-
ws.replyStream(frame, generateReqId('stream'),
|
|
245
|
+
ws.replyStream(frame, generateReqId('stream'), cfg().deniedMessage, true).catch(() => {})
|
|
221
246
|
return
|
|
222
247
|
}
|
|
223
248
|
console.log(`[im-bridge] 收到 from=${sender}: ${content.slice(0, 100)}`)
|
|
@@ -233,7 +258,7 @@ export function apply(ctx, config) {
|
|
|
233
258
|
console.log(`[im-bridge] 用户 ${sender} 进入会话`)
|
|
234
259
|
ws.replyWelcome(frame, {
|
|
235
260
|
msgtype: 'text',
|
|
236
|
-
text: { content:
|
|
261
|
+
text: { content: cfg().welcomeMessage },
|
|
237
262
|
}).catch((e) => console.error(`[im-bridge] 欢迎语失败: ${e.message}`))
|
|
238
263
|
})
|
|
239
264
|
|
package/src/wecom.js
CHANGED
|
@@ -4,6 +4,30 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { generateReqId } from '@wecom/aibot-node-sdk'
|
|
6
6
|
|
|
7
|
+
/** 流式思考动画的默认素材(可被 Config.thinking / cordis patch 覆盖) */
|
|
8
|
+
export const DEFAULT_THINKING = {
|
|
9
|
+
phases: [
|
|
10
|
+
{ atSec: 0, text: '🤔 正在理解你的需求…' },
|
|
11
|
+
{ atSec: 8, text: '📋 正在整理任务清单' },
|
|
12
|
+
{ atSec: 25, text: '🔍 正在查找相关资料' },
|
|
13
|
+
{ atSec: 55, text: '✍️ 正在处理文档/数据' },
|
|
14
|
+
{ atSec: 120, text: '🧠 正在思考最佳方案…' },
|
|
15
|
+
{ atSec: 240, text: '⏳ 任务较繁琐,请稍候…' },
|
|
16
|
+
{ atSec: 420, text: '☕ 快好了,正在收尾…' },
|
|
17
|
+
],
|
|
18
|
+
spin: ['🧠', '💭', '✨', '🔎', '⚡'],
|
|
19
|
+
eggs: [
|
|
20
|
+
'📎 顺手把要点整理好了,稍后一起给你',
|
|
21
|
+
'📶 网络有点忙,让它慢慢跑',
|
|
22
|
+
'🎯 结果快出来了,坚持一下',
|
|
23
|
+
'🗂️ 资料较多,正在汇总中',
|
|
24
|
+
'🌙 别盯着了,完成会自动通知你',
|
|
25
|
+
],
|
|
26
|
+
eggAfterSec: 240,
|
|
27
|
+
intervalMs: 1500,
|
|
28
|
+
activityPrefix: '🛠️ 正在执行 ',
|
|
29
|
+
}
|
|
30
|
+
|
|
7
31
|
/** 毫秒 → 人类可读时长(如 "9 分 47 秒") */
|
|
8
32
|
export function fmtDuration(ms) {
|
|
9
33
|
const s = Math.floor(ms / 1000)
|
|
@@ -37,46 +61,42 @@ export function truncate(text, max) {
|
|
|
37
61
|
}
|
|
38
62
|
|
|
39
63
|
/**
|
|
40
|
-
* 流式动画 v3
|
|
41
|
-
* 每
|
|
42
|
-
* @param {() => string} [activity] - 可选:
|
|
64
|
+
* 流式动画 v3: 阶段化台词 + 旋转表情 + 进度条 + 已用时/剩余估算 + 长任务彩蛋。
|
|
65
|
+
* 每 intervalMs 更新一次同一条流式消息(共用 streamId), 直到 agent 完成。返回停止函数。
|
|
66
|
+
* @param {() => string} [activity] - 可选: 返回当前真实活动描述, 覆盖阶段台词。
|
|
67
|
+
* @param {typeof DEFAULT_THINKING} [thinking] - 动画素材; 缺省用 {@link DEFAULT_THINKING}。
|
|
43
68
|
*/
|
|
44
|
-
export function startThinking(ws, frame, streamId, startedAt, timeoutSec, activity) {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
[240, '⏳ 任务较繁琐,请稍候…'],
|
|
52
|
-
[420, '☕ 快好了,正在收尾…'],
|
|
53
|
-
]
|
|
54
|
-
const SPIN = ['🧠', '💭', '✨', '🔎', '⚡']
|
|
55
|
-
const EGGS = [
|
|
56
|
-
'📎 顺手把要点整理好了,稍后一起给你',
|
|
57
|
-
'📶 网络有点忙,让它慢慢跑',
|
|
58
|
-
'🎯 结果快出来了,坚持一下',
|
|
59
|
-
'🗂️ 资料较多,正在汇总中',
|
|
60
|
-
'🌙 别盯着了,完成会自动通知你',
|
|
61
|
-
]
|
|
69
|
+
export function startThinking(ws, frame, streamId, startedAt, timeoutSec, activity, thinking) {
|
|
70
|
+
const t = { ...DEFAULT_THINKING, ...(thinking || {}) }
|
|
71
|
+
const phases = Array.isArray(t.phases) && t.phases.length > 0 ? t.phases : DEFAULT_THINKING.phases
|
|
72
|
+
const spin = Array.isArray(t.spin) && t.spin.length > 0 ? t.spin : DEFAULT_THINKING.spin
|
|
73
|
+
const eggs = Array.isArray(t.eggs) && t.eggs.length > 0 ? t.eggs : DEFAULT_THINKING.eggs
|
|
74
|
+
const eggAfterSec = Number.isFinite(t.eggAfterSec) ? t.eggAfterSec : DEFAULT_THINKING.eggAfterSec
|
|
75
|
+
const intervalMs = Number.isFinite(t.intervalMs) && t.intervalMs > 0 ? t.intervalMs : DEFAULT_THINKING.intervalMs
|
|
62
76
|
const total = Number.isFinite(timeoutSec) && timeoutSec > 0 ? timeoutSec : 600
|
|
63
77
|
let i = 0
|
|
64
78
|
const timer = setInterval(() => {
|
|
65
79
|
const secs = Math.floor((Date.now() - startedAt) / 1000)
|
|
66
80
|
const live = activity ? activity() : ''
|
|
67
|
-
let stage =
|
|
68
|
-
if (!live)
|
|
81
|
+
let stage = phases[0].text
|
|
82
|
+
if (!live) {
|
|
83
|
+
for (const p of phases) {
|
|
84
|
+
if (secs >= p.atSec) stage = p.text
|
|
85
|
+
}
|
|
86
|
+
}
|
|
69
87
|
const pct = Math.min(Math.floor((secs / total) * 100), 99)
|
|
70
88
|
const filled = '█'.repeat(Math.floor(pct / 10))
|
|
71
89
|
const bar = secs < 3 ? '' : `\n${filled}${'░'.repeat(10 - filled.length)} ${String(pct).padStart(2)}%`
|
|
72
90
|
const remain = Math.max(total - secs, 0)
|
|
73
91
|
const remainTxt = secs < 3 ? '' : ` · 预计还剩 ${Math.floor(remain / 60)}分${remain % 60}秒`
|
|
74
|
-
const egg = secs >=
|
|
75
|
-
|
|
92
|
+
const egg = secs >= eggAfterSec && eggs.length > 0
|
|
93
|
+
? `\n${eggs[Math.floor(secs / 60) % eggs.length]}`
|
|
94
|
+
: ''
|
|
95
|
+
const emoji = spin[i % spin.length]
|
|
76
96
|
i++
|
|
77
97
|
const status = live || stage
|
|
78
98
|
ws.replyStream(frame, streamId, `${emoji} ${status} ⏱ ${secs} 秒${remainTxt}${bar}${egg}`, false).catch(() => {})
|
|
79
|
-
},
|
|
99
|
+
}, intervalMs)
|
|
80
100
|
return () => clearInterval(timer)
|
|
81
101
|
}
|
|
82
102
|
|