zames_pro 2.70.0 → 2.72.0
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 +32 -0
- package/README.md +8 -0
- package/dist/agent-loop.js +258 -857
- package/dist/answer-parse.js +765 -0
- package/dist/browser-chats.js +161 -0
- package/dist/browser-pure.js +238 -0
- package/dist/browser.js +45 -390
- package/dist/changelog.js +29 -0
- package/dist/cli-args.js +48 -0
- package/dist/compact.js +9 -0
- package/dist/config.js +24 -0
- package/dist/diagnostics.js +71 -0
- package/dist/hooks.js +30 -2
- package/dist/i18n/common.js +4 -0
- package/dist/i18n/config.js +8 -0
- package/dist/index.js +81 -24
- package/dist/input.js +25 -1
- package/dist/run-task.js +33 -1
- package/dist/shell.js +102 -1
- package/dist/statusline.js +52 -0
- package/dist/subagent.js +156 -0
- package/dist/system-prompt.js +38 -1
- package/dist/tools.js +105 -9
- package/package.json +10 -8
- package/scripts/changelog-draft.mts +38 -1
- package/scripts/{eslint-ts6.mjs → eslint-ts6.mts} +11 -4
- package/scripts/release.mts +36 -7
- package/scripts/{render-demo.mjs → render-demo.mts} +12 -11
- package/scripts/{self-smoke.mjs → self-smoke.mts} +86 -5
- /package/scripts/{clean-dist.mjs → clean-dist.mts} +0 -0
- /package/scripts/{coverage-gate.mjs → coverage-gate.mts} +0 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// PURE parsing of the DeepSeek history/message payloads (BACKLOG C3). These
|
|
2
|
+
// helpers are extracted from DeepSeekBrowser.fetchChatMessages: they used to be
|
|
3
|
+
// copy-pasted INLINE twice (the in-page evaluate AND the Playwright-request
|
|
4
|
+
// fallback), so the same role/fragment extraction lived in two places and could
|
|
5
|
+
// drift. They operate on plain JSON, touch no browser state, and are
|
|
6
|
+
// unit-tested without Playwright.
|
|
7
|
+
// Collapse 3+ blank lines to one blank line and trim — DeepSeek pads the
|
|
8
|
+
// fragments with newlines and the raw text looked double-spaced in the
|
|
9
|
+
// restored dialogue.
|
|
10
|
+
export function tidyFragmentText(text) {
|
|
11
|
+
const NL = String.fromCharCode(10);
|
|
12
|
+
return String(text || '')
|
|
13
|
+
.replace(new RegExp(NL + '{3,}', 'g'), NL + NL)
|
|
14
|
+
.trim();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Extract displayable messages from `data.biz_data.chat_messages[]`.
|
|
18
|
+
*
|
|
19
|
+
* Assistant turns keep only RESPONSE fragments, user turns only REQUEST
|
|
20
|
+
* fragments (THINK/FILE fragments are dropped, matching the live reader). The
|
|
21
|
+
* LATEST `accumulated_token_usage` seen in the payload is returned as
|
|
22
|
+
* `usage` (each message carries the running counter).
|
|
23
|
+
*/
|
|
24
|
+
export function parseHistoryMessages(messages) {
|
|
25
|
+
if (!Array.isArray(messages))
|
|
26
|
+
return { list: [], usage: null };
|
|
27
|
+
const list = [];
|
|
28
|
+
let usage = null;
|
|
29
|
+
for (const raw of messages) {
|
|
30
|
+
if (raw && typeof raw.accumulated_token_usage === 'number') {
|
|
31
|
+
usage = raw.accumulated_token_usage;
|
|
32
|
+
}
|
|
33
|
+
const role = raw && raw.role === 'ASSISTANT' ? 'assistant' : 'user';
|
|
34
|
+
const want = role === 'assistant' ? 'RESPONSE' : 'REQUEST';
|
|
35
|
+
let text = '';
|
|
36
|
+
for (const fr of (raw && raw.fragments) || []) {
|
|
37
|
+
if (!fr || fr.type !== want)
|
|
38
|
+
continue;
|
|
39
|
+
if (typeof fr.content === 'string')
|
|
40
|
+
text += fr.content;
|
|
41
|
+
}
|
|
42
|
+
text = tidyFragmentText(text);
|
|
43
|
+
if (text)
|
|
44
|
+
list.push({ role, text });
|
|
45
|
+
}
|
|
46
|
+
return { list, usage };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Pull `chat_messages` out of a history_messages JSON response, tolerating the
|
|
50
|
+
* two shapes the endpoint has shipped (`data.biz_data.chat_messages`); returns
|
|
51
|
+
* null when the shape does not match, so the caller can report a clear error.
|
|
52
|
+
*/
|
|
53
|
+
export function extractChatMessages(json) {
|
|
54
|
+
const j = json;
|
|
55
|
+
const messages = j?.data?.biz_data?.chat_messages;
|
|
56
|
+
return Array.isArray(messages) ? messages : null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* In-page scraper for the chat list (the sidebar anchors). Runs INSIDE the
|
|
60
|
+
* page via page.evaluate, so it must NOT close over module scope — it only
|
|
61
|
+
* uses its `limit` argument and DOM globals. Kept as a named export so the
|
|
62
|
+
* class body stays thin and this scraping policy has ONE home.
|
|
63
|
+
*/
|
|
64
|
+
export function scrapeChatList(limit) {
|
|
65
|
+
const out = [];
|
|
66
|
+
const seen = new Set();
|
|
67
|
+
const anchors = document.querySelectorAll('a[href*="/chat/"]');
|
|
68
|
+
for (const a of Array.from(anchors)) {
|
|
69
|
+
const href = a.getAttribute('href') || '';
|
|
70
|
+
const m = href.match(/\/chat\/s\/([a-zA-Z0-9_-]+)/) ||
|
|
71
|
+
href.match(/\/a\/chat\/s\/([a-zA-Z0-9_-]+)/);
|
|
72
|
+
if (!m)
|
|
73
|
+
continue;
|
|
74
|
+
const id = m[1];
|
|
75
|
+
if (seen.has(id))
|
|
76
|
+
continue;
|
|
77
|
+
seen.add(id);
|
|
78
|
+
const titleEl = a.querySelector('[class*="title"], [class*="text"]');
|
|
79
|
+
let title = (titleEl ? titleEl.textContent : a.textContent) || '';
|
|
80
|
+
title = title.trim().replace(/\s+/g, ' ');
|
|
81
|
+
if (!title)
|
|
82
|
+
title = '(без названия)';
|
|
83
|
+
out.push({ id, title, href });
|
|
84
|
+
if (out.length >= limit)
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* In-page scraper for the WHOLE visible dialogue of the open chat. Runs INSIDE
|
|
91
|
+
* the page via page.evaluate (no module-scope closure). Message-level
|
|
92
|
+
* containers are tried first, then assistant-only answer wrappers as a
|
|
93
|
+
* fallback; nested duplicate blocks are collapsed to the OUTERMOST one (some
|
|
94
|
+
* builds match both an outer and an inner container, which printed each turn
|
|
95
|
+
* twice). The model's reasoning (think class) is skipped.
|
|
96
|
+
*/
|
|
97
|
+
export function scrapeChatMessages(opts) {
|
|
98
|
+
const think = new RegExp(opts.thinkRe, 'i');
|
|
99
|
+
const assistantRe = new RegExp(opts.assistantRe, 'i');
|
|
100
|
+
const inThink = (e) => {
|
|
101
|
+
let n = e;
|
|
102
|
+
while (n) {
|
|
103
|
+
const cls = (n.className || '').toString();
|
|
104
|
+
if (think.test(cls))
|
|
105
|
+
return true;
|
|
106
|
+
n = n.parentElement;
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
};
|
|
110
|
+
const textOf = (e) => {
|
|
111
|
+
const h = e;
|
|
112
|
+
const t = h.innerText || h.textContent || '';
|
|
113
|
+
const NL = String.fromCharCode(10);
|
|
114
|
+
return t.replace(new RegExp(NL + '{3,}', 'g'), NL + NL).trim();
|
|
115
|
+
};
|
|
116
|
+
const looksAssistant = (e) => {
|
|
117
|
+
const cls = (e.className || '').toString();
|
|
118
|
+
if (assistantRe.test(cls))
|
|
119
|
+
return true;
|
|
120
|
+
if (e.querySelector(opts.assistantSel))
|
|
121
|
+
return true;
|
|
122
|
+
if (e.querySelector(opts.markdownSel))
|
|
123
|
+
return true;
|
|
124
|
+
return false;
|
|
125
|
+
};
|
|
126
|
+
let blocks = [];
|
|
127
|
+
for (const s of opts.containerSels) {
|
|
128
|
+
const found = Array.from(document.querySelectorAll(s)).filter((e) => !inThink(e) && textOf(e).length > 0);
|
|
129
|
+
if (found.length) {
|
|
130
|
+
blocks = found;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (blocks.length > 1) {
|
|
135
|
+
const unique = Array.from(new Set(blocks));
|
|
136
|
+
blocks = unique.filter((b) => !unique.some((other) => other !== b && other.contains(b)));
|
|
137
|
+
}
|
|
138
|
+
const out = [];
|
|
139
|
+
if (blocks.length) {
|
|
140
|
+
for (const b of blocks) {
|
|
141
|
+
const t = textOf(b);
|
|
142
|
+
if (!t)
|
|
143
|
+
continue;
|
|
144
|
+
out.push({ role: looksAssistant(b) ? 'assistant' : 'user', text: t });
|
|
145
|
+
}
|
|
146
|
+
if (out.length)
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
for (const s of opts.answerSels) {
|
|
150
|
+
const list = Array.from(document.querySelectorAll(s)).filter((e) => !inThink(e));
|
|
151
|
+
if (!list.length)
|
|
152
|
+
continue;
|
|
153
|
+
for (const el of list) {
|
|
154
|
+
const t = textOf(el);
|
|
155
|
+
if (t)
|
|
156
|
+
out.push({ role: 'assistant', text: t });
|
|
157
|
+
}
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import { chromium } from 'playwright';
|
|
5
|
+
import { GENERATION_ERR_RE, INCOMPLETE_STATUS_RE, RATE_LIMIT_RE, SERVER_BUSY_RE, } from './deepseek-ui.js';
|
|
6
|
+
// Pure, state-free helpers extracted from browser.ts (BACKLOG C3). Nothing
|
|
7
|
+
// here touches the DeepSeekBrowser instance state, so this module is safe to
|
|
8
|
+
// import anywhere (including hot-reloaded modules) without pulling in the
|
|
9
|
+
// live Playwright context. browser.ts re-exports the public names, so the
|
|
10
|
+
// existing API is unchanged.
|
|
11
|
+
// Muted terminal input for passwords. We do NOT use readline here: readline
|
|
12
|
+
// echoes the typed text through its own internal _writeToOutput, which cannot
|
|
13
|
+
// be reliably overridden from outside (a real bug: the login prompt swallowed
|
|
14
|
+
// the password step). Instead we read raw keystrokes from stdin and echo one
|
|
15
|
+
// `*` per typed character. In a non-TTY (pipe/redirect) we fall back to a
|
|
16
|
+
// plain line read.
|
|
17
|
+
export function askPassword(question) {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
process.stdout.write(question);
|
|
20
|
+
// Non-interactive: read a single line from stdin as-is.
|
|
21
|
+
if (!process.stdin.isTTY) {
|
|
22
|
+
void import('readline').then(({ createInterface }) => {
|
|
23
|
+
const rl = createInterface({
|
|
24
|
+
input: process.stdin,
|
|
25
|
+
output: process.stdout,
|
|
26
|
+
});
|
|
27
|
+
rl.question('', (answer) => {
|
|
28
|
+
rl.close();
|
|
29
|
+
resolve(answer);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const stdin = process.stdin;
|
|
35
|
+
const wasRaw = stdin.isRaw;
|
|
36
|
+
stdin.setRawMode(true);
|
|
37
|
+
stdin.resume();
|
|
38
|
+
let value = '';
|
|
39
|
+
const onData = (buf) => {
|
|
40
|
+
const s = buf.toString('utf8');
|
|
41
|
+
for (const ch of s) {
|
|
42
|
+
const code = ch.charCodeAt(0);
|
|
43
|
+
// Enter / Ctrl+J -> done.
|
|
44
|
+
if (ch === '\r' || ch === '\n') {
|
|
45
|
+
done();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// Ctrl+C -> abort the whole process (same as elsewhere in the CLI).
|
|
49
|
+
if (code === 3) {
|
|
50
|
+
cleanup();
|
|
51
|
+
process.stdout.write('\n');
|
|
52
|
+
process.exit(130);
|
|
53
|
+
}
|
|
54
|
+
// Ctrl+D -> finish with whatever we have.
|
|
55
|
+
if (code === 4) {
|
|
56
|
+
done();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Backspace (0x7f or 0x08): remove the last char and one star.
|
|
60
|
+
if (code === 127 || code === 8) {
|
|
61
|
+
if (value.length > 0) {
|
|
62
|
+
value = value.slice(0, -1);
|
|
63
|
+
process.stdout.write('\b \b');
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
// Ignore other control characters (arrows, etc.).
|
|
68
|
+
if (code < 32)
|
|
69
|
+
continue;
|
|
70
|
+
value += ch;
|
|
71
|
+
process.stdout.write('*');
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const cleanup = () => {
|
|
75
|
+
stdin.removeListener('data', onData);
|
|
76
|
+
try {
|
|
77
|
+
stdin.setRawMode(wasRaw || false);
|
|
78
|
+
}
|
|
79
|
+
catch { }
|
|
80
|
+
stdin.pause();
|
|
81
|
+
};
|
|
82
|
+
const done = () => {
|
|
83
|
+
cleanup();
|
|
84
|
+
process.stdout.write('\n');
|
|
85
|
+
resolve(value);
|
|
86
|
+
};
|
|
87
|
+
stdin.on('data', onData);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
export const USER_DATA_DIR = path.join(os.homedir(), '.zames', 'profile');
|
|
91
|
+
// Cache of the sanitized (non-Headless) User-Agent, keyed by the browser
|
|
92
|
+
// engine path. On the FIRST headless run we detect the real UA, strip the
|
|
93
|
+
// "Headless" marker and store it here; later runs pass it straight to
|
|
94
|
+
// _launchOnce, so the browser starts with a good UA and the "launch → read UA →
|
|
95
|
+
// relaunch" dance happens only once (it otherwise doubled every headless
|
|
96
|
+
// start). Keying by the executable path means a Playwright update (new engine
|
|
97
|
+
// path) invalidates the cache and we re-derive the UA once for the new build.
|
|
98
|
+
const HEADLESS_UA_CACHE = path.join(os.homedir(), '.zames', 'headless-ua.json');
|
|
99
|
+
// Pure: pick a reusable UA from the cache file's text for the given engine, or
|
|
100
|
+
// null. Exported so the policy is unit-tested without touching ~/.zames.
|
|
101
|
+
export function parseHeadlessUACache(raw, engine) {
|
|
102
|
+
try {
|
|
103
|
+
const j = JSON.parse(raw);
|
|
104
|
+
if (j &&
|
|
105
|
+
j.engine === engine &&
|
|
106
|
+
typeof j.ua === 'string' &&
|
|
107
|
+
j.ua &&
|
|
108
|
+
!/Headless/i.test(j.ua)) {
|
|
109
|
+
return j.ua;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch { }
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
export function headlessEngineKey() {
|
|
116
|
+
try {
|
|
117
|
+
return chromium.executablePath();
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return '';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export async function readCachedHeadlessUA(engine) {
|
|
124
|
+
if (!engine)
|
|
125
|
+
return null;
|
|
126
|
+
try {
|
|
127
|
+
const raw = await fs.readFile(HEADLESS_UA_CACHE, 'utf-8');
|
|
128
|
+
return parseHeadlessUACache(raw, engine);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export async function writeCachedHeadlessUA(engine, ua) {
|
|
135
|
+
if (!engine || !ua)
|
|
136
|
+
return;
|
|
137
|
+
try {
|
|
138
|
+
await fs.mkdir(path.dirname(HEADLESS_UA_CACHE), { recursive: true });
|
|
139
|
+
await fs.writeFile(HEADLESS_UA_CACHE, JSON.stringify({ engine, ua }, null, 2), 'utf-8');
|
|
140
|
+
}
|
|
141
|
+
catch { }
|
|
142
|
+
}
|
|
143
|
+
// For /doctor: is a sanitized UA cached for the CURRENT engine build? A missing
|
|
144
|
+
// cache only means the next headless start relaunches once (not an error), so
|
|
145
|
+
// this is informational. Best-effort.
|
|
146
|
+
export async function headlessUACacheReady() {
|
|
147
|
+
const ua = await readCachedHeadlessUA(headlessEngineKey());
|
|
148
|
+
return !!ua;
|
|
149
|
+
}
|
|
150
|
+
// A headless Chrome advertises "HeadlessChrome/..." in its User-Agent, and
|
|
151
|
+
// DeepSeek's CDN (CloudFront/WAF) rejects that UA with a plain "403 ERROR"
|
|
152
|
+
// page before the app is even served — so a headless login looked broken while
|
|
153
|
+
// a headed one worked (headed Chrome sends "Chrome/..."). We keep the real
|
|
154
|
+
// engine version and only drop the "Headless" marker. When the operator
|
|
155
|
+
// explicitly set a UA in the config, that one wins (see _launchOnce).
|
|
156
|
+
export function sanitizeHeadlessUA(ua) {
|
|
157
|
+
return String(ua || '').replace(/HeadlessChrome/g, 'Chrome');
|
|
158
|
+
}
|
|
159
|
+
// Written after a successful sign-in so the next launch knows a session was
|
|
160
|
+
// stored in the persistent profile (used by /doctor and diagnostics).
|
|
161
|
+
export const AUTH_MARKER_FILE = path.join(os.homedir(), '.zames', 'auth.json');
|
|
162
|
+
export function isGenerationIncompleteText(text) {
|
|
163
|
+
const t = String(text || '');
|
|
164
|
+
return GENERATION_ERR_RE.test(t) || INCOMPLETE_STATUS_RE.test(t);
|
|
165
|
+
}
|
|
166
|
+
export function isRateLimitText(text) {
|
|
167
|
+
return RATE_LIMIT_RE.test(String(text || ''));
|
|
168
|
+
}
|
|
169
|
+
export function isServerBusyText(text) {
|
|
170
|
+
const t = String(text || '');
|
|
171
|
+
// Rate limit takes priority (it needs the long wait).
|
|
172
|
+
if (RATE_LIMIT_RE.test(t))
|
|
173
|
+
return false;
|
|
174
|
+
return SERVER_BUSY_RE.test(t);
|
|
175
|
+
}
|
|
176
|
+
// Normalize text for comparison: collapse whitespace so that tiny DOM
|
|
177
|
+
// differences (nbsp, trailing spaces) don't count as "a new answer".
|
|
178
|
+
export function normText(s) {
|
|
179
|
+
return String(s || '')
|
|
180
|
+
.replace(/\u00a0/g, ' ')
|
|
181
|
+
.replace(/\s+/g, ' ')
|
|
182
|
+
.trim();
|
|
183
|
+
}
|
|
184
|
+
// The "too frequent" error: distinct from others so ask() waits a long time
|
|
185
|
+
// (DeepSeek limits reset over minutes) and retries the send itself.
|
|
186
|
+
export class RateLimitError extends Error {
|
|
187
|
+
constructor(detail) {
|
|
188
|
+
super('Messages too frequent. Try again later. ' + detail);
|
|
189
|
+
this.name = 'RateLimitError';
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// DeepSeek transient server error (Server busy).
|
|
193
|
+
export class ServerBusyError extends Error {
|
|
194
|
+
constructor(detail) {
|
|
195
|
+
super('Server busy. Try again later. ' + detail);
|
|
196
|
+
this.name = 'ServerBusyError';
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// The generation was truncated by the server (`generation_err` / INCOMPLETE).
|
|
200
|
+
// Distinct from ServerBusyError: the DeepSeek backend answered, but the turn
|
|
201
|
+
// did not finish, and the UI offers a "Continue" button. The agent retries the
|
|
202
|
+
// same send (a new message into the chat) instead of waiting for an answer
|
|
203
|
+
// that will never arrive. This is what the operator hit with the reasoning
|
|
204
|
+
// (deep thinking) mode on: the long THINK phase makes the truncation far more
|
|
205
|
+
// likely, so with thinking off everything worked and with it on the turn kept
|
|
206
|
+
// stopping and showing "Continue".
|
|
207
|
+
export class GenerationIncompleteError extends Error {
|
|
208
|
+
constructor(detail) {
|
|
209
|
+
super('Generation incomplete (server truncated the answer). ' + detail);
|
|
210
|
+
this.name = 'GenerationIncompleteError';
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// ---------- profile cleanup ----------
|
|
214
|
+
export async function cleanSingletonFiles() {
|
|
215
|
+
const names = ['SingletonLock', 'SingletonCookie', 'SingletonSocket'];
|
|
216
|
+
for (const name of names) {
|
|
217
|
+
const p = path.join(USER_DATA_DIR, name);
|
|
218
|
+
try {
|
|
219
|
+
await fs.unlink(p);
|
|
220
|
+
}
|
|
221
|
+
catch { }
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
export async function profileLooksLocked() {
|
|
225
|
+
try {
|
|
226
|
+
await fs.access(path.join(USER_DATA_DIR, 'SingletonLock'));
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
export async function nukeProfile() {
|
|
234
|
+
try {
|
|
235
|
+
await fs.rm(USER_DATA_DIR, { recursive: true, force: true });
|
|
236
|
+
}
|
|
237
|
+
catch { }
|
|
238
|
+
}
|