@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 +39 -0
- package/lib/index.js +71 -7
- package/package.json +1 -1
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
|
-
/**
|
|
28
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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