@peng272/dsh-wechat-ilink 0.7.0 → 0.7.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/CHANGELOG.md CHANGED
@@ -15,6 +15,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
15
15
  > documentation this project has: every one of them was an assumed DSH contract
16
16
  > that turned out to be wrong.
17
17
 
18
+ ## [0.7.2]
19
+
20
+ ### Fixed
21
+
22
+ - **The startup banner reported the wrong version.** `PLUGIN_VERSION` was a
23
+ hardcoded literal mirrored from `package.json`, and it drifted on the first
24
+ release that bumped the version: 0.7.1 was loaded while the banner said 0.7.0.
25
+ Since the banner is deliberately the way a loaded build is identified, that
26
+ defeats its purpose. The version is now read from this package's own
27
+ `package.json` at load, so it cannot drift again.
28
+ ## [0.7.1]
29
+
30
+ ### Fixed
31
+
32
+ - **A fresh conversation could never start.** Every turn passed
33
+ `--session-id <sessionId>`, but `dsh --profile headless --session-id` requires
34
+ that session to already exist. On a new install the recorded id
35
+ (`wechat-clawbot`) does not exist yet, so **every** inbound message failed with
36
+
37
+ ```
38
+ dsh: session "wechat-clawbot" does not exist; omit --session-id to start a new Session
39
+ ```
40
+
41
+ and produced no reply. The first turn now omits the flag (which creates the
42
+ session), the id the driver reports is persisted to `<stateDir>/session.json`,
43
+ and later turns resume it. A recorded session that has since disappeared is
44
+ detected and transparently restarted.
45
+
46
+ Found by reading the channel log of a live install; the failure was silent from
47
+ WeChat's side (the message simply got no answer).
48
+
49
+ - Driver-level failures emitted as a `{"type":"error"}` frame are now surfaced in
50
+ the log instead of being reported only as `reason=incomplete`.
51
+
52
+ ### Changed
53
+
54
+ - The package is published as **`@peng272/dsh-wechat-ilink`**. The bare name
55
+ `dsh-wechat-ilink` belongs to another maintainer and cannot be updated from
56
+ this account.
18
57
  ## [0.7.0]
19
58
 
20
59
  A reimplementation of the channel internals. The **runtime identities are
package/lib/index.js CHANGED
@@ -16,6 +16,7 @@ import fs from 'node:fs'
16
16
  import path from 'node:path'
17
17
  import { spawn } from 'node:child_process'
18
18
  import { randomUUID } from 'node:crypto'
19
+ import { fileURLToPath } from 'node:url'
19
20
 
20
21
  import { AccountStore, resolveStateDir } from './accounts.js'
21
22
  import { ILinkClient, QrLoginManager, DEFAULT_BASE_URL, renderQrPng } from './ilink.js'
@@ -24,8 +25,25 @@ import { Config } from './schema.js'
24
25
 
25
26
  export const name = 'wechat-clawbot'
26
27
 
27
- /** Plugin version, mirrored from package.json (a literal keeps the bundle JSON-free). */
28
- export const PLUGIN_VERSION = '0.7.0'
28
+ /**
29
+ * Plugin version, read from this package's own `package.json`.
30
+ *
31
+ * This used to be a hardcoded literal mirrored from package.json, and it drifted
32
+ * on the very first release that bumped the version — the banner advertised
33
+ * 0.7.0 while 0.7.1 was loaded, which defeats the point of having a banner.
34
+ * Reading the file costs one synchronous read at load and cannot drift.
35
+ */
36
+ function readOwnVersion() {
37
+ try {
38
+ const here = path.dirname(fileURLToPath(import.meta.url))
39
+ const pkg = JSON.parse(fs.readFileSync(path.join(here, '..', 'package.json'), 'utf-8'))
40
+ return typeof pkg.version === 'string' && pkg.version ? pkg.version : '0.0.0'
41
+ } catch {
42
+ return '0.0.0'
43
+ }
44
+ }
45
+
46
+ export const PLUGIN_VERSION = readOwnVersion()
29
47
 
30
48
  /**
31
49
  * Services this plugin requires before it activates.
@@ -102,6 +120,7 @@ export function parseHeadlessJson(stdout) {
102
120
  let sessionId
103
121
  let text = ''
104
122
  let reason
123
+ let directError
105
124
  for (const line of String(stdout ?? '').split('\n')) {
106
125
  const trimmed = line.trim()
107
126
  if (!trimmed) continue
@@ -114,12 +133,15 @@ export function parseHeadlessJson(stdout) {
114
133
  if (event.type === 'session' && typeof event.sessionId === 'string') sessionId = event.sessionId
115
134
  else if (event.type === 'final' && typeof event.text === 'string') text = event.text
116
135
  else if (event.type === 'status' && event.phase === 'turn_end' && event.reason) reason = event.reason
136
+ // A driver-level failure (for example an unknown --session-id) is emitted as
137
+ // its own frame, not as a turn_end reason.
138
+ else if (event.type === 'error' && typeof event.message === 'string') directError = event.message
117
139
  }
118
140
  return {
119
141
  sessionId,
120
142
  text,
121
143
  reason: reason ? reason.kind : 'incomplete',
122
- error: reason?.error?.message,
144
+ error: reason?.error?.message ?? directError,
123
145
  }
124
146
  }
125
147
 
@@ -215,9 +237,39 @@ export function apply(ctx, rawConfig) {
215
237
  let sessionId = config.sessionId?.trim() || 'wechat-clawbot'
216
238
  let chain = Promise.resolve()
217
239
 
218
- function runHeadless(prompt) {
219
- if (!dshPrefix) return Promise.resolve({ text: '', reason: 'no-dsh-prefix' })
220
- const argv = [...dshPrefix, '--profile', 'headless', '--json', '--session-id', sessionId, prompt]
240
+ // `dsh --profile headless --session-id <id>` requires that session to ALREADY
241
+ // exist. Passing it for a fresh conversation fails every single turn with
242
+ // session "<id>" does not exist; omit --session-id to start a new Session
243
+ // so the first run omits the flag and the id it reports is remembered for the
244
+ // turns after it. A recorded session that has since disappeared is detected
245
+ // from stderr and retried without the flag.
246
+ const sessionStateFile = path.join(stateDir, 'session.json')
247
+ let sessionEstablished = false
248
+ try {
249
+ if (fs.existsSync(sessionStateFile)) {
250
+ const saved = JSON.parse(fs.readFileSync(sessionStateFile, 'utf-8'))
251
+ if (typeof saved.id === 'string' && saved.id) sessionId = saved.id
252
+ sessionEstablished = saved.established === true
253
+ }
254
+ } catch {
255
+ /* start a fresh conversation */
256
+ }
257
+
258
+ function rememberSession(id, established) {
259
+ if (id) sessionId = id
260
+ sessionEstablished = established
261
+ try {
262
+ fs.mkdirSync(stateDir, { recursive: true })
263
+ fs.writeFileSync(sessionStateFile, JSON.stringify({ id: sessionId, established }, null, 2), 'utf-8')
264
+ } catch (error) {
265
+ log('error', `could not persist session state: ${error?.message ?? error}`)
266
+ }
267
+ }
268
+
269
+ function spawnHeadless(prompt, useSessionId) {
270
+ const argv = [...dshPrefix, '--profile', 'headless', '--json']
271
+ if (useSessionId) argv.push('--session-id', sessionId)
272
+ argv.push(prompt)
221
273
  return new Promise((resolve) => {
222
274
  let child
223
275
  try {
@@ -256,11 +308,23 @@ export function apply(ctx, rawConfig) {
256
308
  if (parsed.reason === 'incomplete' && stderr.trim()) {
257
309
  log('error', `headless exited code=${code}: ${stderr.trim().slice(0, 400)}`)
258
310
  }
259
- finish(parsed)
311
+ finish({ ...parsed, stderr, exitCode: code })
260
312
  })
261
313
  })
262
314
  }
263
315
 
316
+ async function runHeadless(prompt) {
317
+ if (!dshPrefix) return { text: '', reason: 'no-dsh-prefix' }
318
+ let result = await spawnHeadless(prompt, sessionEstablished)
319
+ if (result.reason === 'incomplete' && /does not exist|omit --session-id/i.test(result.stderr || '')) {
320
+ log('error', 'the recorded session is gone; starting a new conversation')
321
+ rememberSession(sessionId, false)
322
+ result = await spawnHeadless(prompt, false)
323
+ }
324
+ if (result.sessionId) rememberSession(result.sessionId, true)
325
+ return result
326
+ }
327
+
264
328
  // ---- inbound ----
265
329
  function extractText(msg) {
266
330
  const items = Array.isArray(msg?.item_list) ? msg.item_list : []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peng272/dsh-wechat-ilink",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "WeChat ClawBot (Tencent iLink) channel for DeepSeek Harness 0.2.0-rc.2 — talk to DSH from WeChat.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",