@mhfire/dsh-im-bridge 0.1.0 → 0.1.2

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.md CHANGED
@@ -23,11 +23,13 @@ dsh plugin --profile web add <本包路径>
23
23
 
24
24
  重启 dsh 进程即可使用。
25
25
 
26
+ 未配置 `botId` / `secret` 时插件仍会加载(不阻塞 `dsh web`),日志会提示跳过企微连线;Settings → 插件配置页仍可填写,保存后**重启**进程才会连接(本版本不做热启连)。
27
+
26
28
  ## 配置项
27
29
 
28
30
  | 字段 | 说明 |
29
31
  |---|---|
30
- | `botId` / `secret` | 企业微信智能机器人凭证(`role('secret')`,UI 自动脱敏) |
32
+ | `botId` / `secret` | 企业微信智能机器人凭证(`role('secret')`,UI 自动脱敏);缺省时跳过企微侧,不阻塞主进程 |
31
33
  | `workspace` | Agent 工作目录(会话 cwd) |
32
34
  | `allowFrom` | 允许的发送者 userid 白名单;空 = 允许所有人 |
33
35
  | `agentTimeoutSec` | 单任务最长执行时间(秒),动画进度条/剩余估算的基准 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhfire/dsh-im-bridge",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "企业微信智能机器人 ⇄ DeepSeek Harness Agent 桥接插件:进程内创建 Agent(per-sender 持久会话),会话在 GUI 实时可见;含 Settings 插件配置卡片",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/index.js CHANGED
@@ -27,10 +27,10 @@ export const name = 'im-bridge'
27
27
  /** 依赖的核心服务 */
28
28
  export const inject = ['agents', 'sessions', 'agentDefaultModel']
29
29
 
30
- /** 插件配置(zod): 敏感项由 profile 层 patch 提供 */
30
+ /** 插件配置(zod): 敏感项由 profile 层 patch Settings 提供;缺省时跳过企微连线,不阻塞主进程 */
31
31
  export const Config = z.object({
32
- botId: z.string().required().role('secret'),
33
- secret: z.string().required().role('secret'),
32
+ botId: z.string().default('').role('secret'),
33
+ secret: z.string().default('').role('secret'),
34
34
  /** Agent 的工作目录(会话 cwd) */
35
35
  workspace: z.string().default(process.cwd()),
36
36
  /** 允许的发送者 userid 白名单; 空 = 允许所有人 */
@@ -90,75 +90,86 @@ export function apply(ctx, config) {
90
90
  throw new Error('im-bridge: 需要 agents/sessions/agentDefaultModel 服务')
91
91
  }
92
92
 
93
- // per-sender 持久会话状态: sender -> { agent, sessionId, queue, lastActivity }
94
- const senders = new Map()
95
- const persona = resolvePersona(config)
96
-
97
93
  // ── 设置命名空间: 用户可在 GUI 插件配置页 / settings.yaml 覆盖字段(热生效) ──
98
- const settings = ctx.get('settings')
94
+ // 规范做法(与 @deepseek-ai/dsh-settings installSettingsSection 一致):
95
+ // 用 ctx.inject(['settings'], cb) 延迟到 settings 服务可用时再注册,
96
+ // 避免 apply 时 settings 尚未挂载导致命名空间缺失(GUI 卡片显示"命名空间不可用")。
97
+ // 缺凭证时仍注册, 便于在 Settings 补齐后再重启启用企微。
99
98
  let scope
100
- if (settings !== undefined) {
101
- scope = settings.register('im-bridge', Config, { base: { ...config } })
102
- }
99
+ ctx.inject(['settings'], (sctx) => {
100
+ scope = sctx.settings.register('im-bridge', Config, { base: { ...config } })
101
+ })
103
102
  /** 有效配置: 默认值 → cordis patch(base) → 用户 settings.yaml, 热更新 */
104
103
  const cfg = () => scope?.value ?? config
105
104
 
106
- /** 创建(或复用)某发送者的 Agent(会话/上下文持久于进程内) */
107
- async function ensureAgent(sender) {
108
- let st = senders.get(sender)
109
- if (st !== undefined && st.agent !== undefined) return st
110
- const sessionId = SessionId(`session-${randomUUID()}`)
111
- const selection = defaultModel.currentSelection()
112
- // preset roster 的部署(如 web profile)必须在 setup mount,
113
- // 否则 agent 看不到任何工具(模型只能编造工具调用)
114
- const presets = ctx.get('agentPresets')
115
- let resolvedId = cfg().agentPreset
116
- if (presets !== undefined) {
117
- resolvedId = (await presets.resolve(cfg().agentPreset)).id
105
+ // ── 企微 WebSocket: 延迟到 loader settle 后启动, 不占启动关键路径 ──
106
+ void (async () => {
107
+ await ctx.get('loader')?.await()
108
+ const { botId, secret } = cfg()
109
+ if (!botId || !secret) {
110
+ console.warn(
111
+ '[im-bridge] 跳过启动: 缺少 botId/secret。请在 profile cordis.patch.yml Settings 插件配置中填写后重启。',
112
+ )
113
+ return
118
114
  }
119
- const { agent } = await agents.create({
120
- sessionId,
121
- meta: { cwd: cfg().workspace, agentPreset: resolvedId },
122
- agentOptions: { provider: selection.provider, model: selection.model },
123
- setup: async (agentCtx) => {
124
- const selected = { current: selection, assembled: undefined }
125
- installModelSelection(agentCtx, selected)
126
- if (presets !== undefined) await presets.mount(agentCtx, resolvedId)
127
- if (persona !== '') {
128
- agentCtx.inject(['systemPrompt'], (promptCtx) => {
129
- promptCtx.systemPrompt.section({
130
- name: 'deployment:persona', // 同名 scoped section 覆盖部署 persona(仅本 agent)
131
- order: 0,
132
- text: persona,
133
- })
134
- })
135
- }
136
- },
137
- })
138
- st = { agent, sessionId, queue: Promise.resolve(), lastActivity: '' }
139
- senders.set(sender, st)
140
- console.log(`[im-bridge] 为 ${sender} 创建会话 ${sessionId}`)
141
- return st
142
- }
143
115
 
144
- // 会话事件 真实活动状态(工具调用名), 让动画显示"正在做什么"
145
- ctx.on('session/event', (session, event) => {
146
- if (event.type !== 'tool/call') return
147
- for (const st of senders.values()) {
148
- if (st.sessionId === session.id) st.lastActivity = `🛠️ 正在执行 ${event.data.name}`
116
+ // per-sender 持久会话状态: sender -> { agent, sessionId, queue, lastActivity }
117
+ const senders = new Map()
118
+ const persona = resolvePersona(cfg())
119
+
120
+ /** 创建(或复用)某发送者的 Agent(会话/上下文持久于进程内) */
121
+ async function ensureAgent(sender) {
122
+ let st = senders.get(sender)
123
+ if (st !== undefined && st.agent !== undefined) return st
124
+ const sessionId = SessionId(`session-${randomUUID()}`)
125
+ const selection = defaultModel.currentSelection()
126
+ // 有 preset roster 的部署(如 web profile)必须在 setup 里 mount,
127
+ // 否则 agent 看不到任何工具(模型只能编造工具调用)。
128
+ const presets = ctx.get('agentPresets')
129
+ let resolvedId = cfg().agentPreset
130
+ if (presets !== undefined) {
131
+ resolvedId = (await presets.resolve(cfg().agentPreset)).id
132
+ }
133
+ const { agent } = await agents.create({
134
+ sessionId,
135
+ meta: { cwd: cfg().workspace, agentPreset: resolvedId },
136
+ agentOptions: { provider: selection.provider, model: selection.model },
137
+ setup: async (agentCtx) => {
138
+ const selected = { current: selection, assembled: undefined }
139
+ installModelSelection(agentCtx, selected)
140
+ if (presets !== undefined) await presets.mount(agentCtx, resolvedId)
141
+ if (persona !== '') {
142
+ agentCtx.inject(['systemPrompt'], (promptCtx) => {
143
+ promptCtx.systemPrompt.section({
144
+ name: 'deployment:persona', // 同名 scoped section 覆盖部署 persona(仅本 agent)
145
+ order: 0,
146
+ text: persona,
147
+ })
148
+ })
149
+ }
150
+ },
151
+ })
152
+ st = { agent, sessionId, queue: Promise.resolve(), lastActivity: '' }
153
+ senders.set(sender, st)
154
+ console.log(`[im-bridge] 为 ${sender} 创建会话 ${sessionId}`)
155
+ return st
149
156
  }
150
- })
151
157
 
152
- // ── 企微 WebSocket: 延迟到 loader settle 后启动, 不占启动关键路径 ──
153
- void (async () => {
154
- await ctx.get('loader')?.await()
155
- const { default: AiBot } = await import('@wecom/aibot-node-sdk')
158
+ // 会话事件 真实活动状态(工具调用名), 让动画显示"正在做什么"
159
+ ctx.on('session/event', (session, event) => {
160
+ if (event.type !== 'tool/call') return
161
+ for (const st of senders.values()) {
162
+ if (st.sessionId === session.id) st.lastActivity = `🛠️ 正在执行 ${event.data.name}`
163
+ }
164
+ })
165
+
166
+ const { default: AiBot, generateReqId } = await import('@wecom/aibot-node-sdk')
156
167
 
157
168
  /** 处理一条消息: 动画 → followup → 汇总 → 简报 */
158
169
  async function handle(frame, sender, content) {
159
170
  const st = await ensureAgent(sender)
160
171
  const startedAt = Date.now()
161
- const streamId = AiBot.generateReqId('stream')
172
+ const streamId = generateReqId('stream')
162
173
  let stopThinking = null
163
174
  try {
164
175
  await ws.replyStream(frame, streamId, cfg().startHint, false)
@@ -193,7 +204,7 @@ export function apply(ctx, config) {
193
204
  }
194
205
  }
195
206
 
196
- const ws = new AiBot.WSClient({ botId: cfg().botId, secret: cfg().secret })
207
+ const ws = new AiBot.WSClient({ botId, secret })
197
208
 
198
209
  ws.on('connected', () => console.log('[im-bridge] WebSocket 已连接'))
199
210
  ws.on('authenticated', () => console.log('[im-bridge] 认证成功, 等待消息...'))
@@ -206,7 +217,7 @@ export function apply(ctx, config) {
206
217
  if (!content) return
207
218
  const sender = frame.body?.sender?.userid || frame.body?.from?.userid || frame.body?.userid || 'unknown'
208
219
  if (cfg().allowFrom.length > 0 && !cfg().allowFrom.includes(sender)) {
209
- ws.replyStream(frame, AiBot.generateReqId('stream'), '无权访问本服务', true).catch(() => {})
220
+ ws.replyStream(frame, generateReqId('stream'), '无权访问本服务', true).catch(() => {})
210
221
  return
211
222
  }
212
223
  console.log(`[im-bridge] 收到 from=${sender}: ${content.slice(0, 100)}`)
package/src/wecom.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * wecom.js — 企业微信交互封装: 流式动画(v3) + 最终简报 + 发送助手。
3
3
  * 逻辑移植自旧版 im-bridge/bridge.js, 改为纯 ESM 供插件使用。
4
4
  */
5
- import AiBot from '@wecom/aibot-node-sdk'
5
+ import { generateReqId } from '@wecom/aibot-node-sdk'
6
6
 
7
7
  /** 毫秒 → 人类可读时长(如 "9 分 47 秒") */
8
8
  export function fmtDuration(ms) {
@@ -86,6 +86,6 @@ export async function sendFinal(ws, frame, streamId, content) {
86
86
  await ws.replyStream(frame, streamId, content, true)
87
87
  } catch (e) {
88
88
  console.error(`[im-bridge] 原流最终回复失败(${e.message}), 尝试新流...`)
89
- await ws.replyStream(frame, AiBot.generateReqId('stream'), content, true)
89
+ await ws.replyStream(frame, generateReqId('stream'), content, true)
90
90
  }
91
91
  }