zames_pro 2.78.3 → 2.79.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 +20 -0
- package/dist/browser-chats.js +9 -1
- package/dist/commands.js +13 -2
- package/dist/config.js +6 -0
- package/dist/extraTools.js +98 -60
- package/dist/goal.js +27 -4
- package/dist/input/layout.js +22 -8
- package/dist/permissions.js +41 -8
- package/dist/qwen-chats.js +34 -10
- package/dist/qwen-net-capture.js +6 -1
- package/dist/scheduler.js +19 -2
- package/dist/web.js +27 -1
- package/dist/xml-toolcall.js +61 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.79.0] - 2026-10-19
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **qwen:** match the history 'from' role case-insensitively and accept human
|
|
15
|
+
- **browser-chats:** match the history role case-insensitively and drop service turns
|
|
16
|
+
- **extratools:** apply multi-hunk ApplyPatch updates and honour @@ line numbers
|
|
17
|
+
- **web:** drop non-http(s) links from WebSearch results
|
|
18
|
+
- **web:** judge 6to4 and Teredo addresses by the embedded IPv4
|
|
19
|
+
- **permissions:** match path rules against arrays and nested MCP args
|
|
20
|
+
- **scheduler:** reject empty and malformed cron field tokens
|
|
21
|
+
- **qwen:** read numeric and map-keyed ids in the chat list
|
|
22
|
+
- **xml-toolcall:** a stray brace before inline JSON no longer eats the args
|
|
23
|
+
- **xml-toolcall:** keep a </parameter> literal inside a parameter value
|
|
24
|
+
- **qwen:** detect a finished status carried on the choice, not only the delta
|
|
25
|
+
- **config:** reject an empty value for a number setting
|
|
26
|
+
- **goal:** an echoed label set is not the evaluator verdict
|
|
27
|
+
- **input:** expand each paste into its own marker occurrence
|
|
28
|
+
- **compact:** a summary that mentions DSML in prose is usable
|
|
29
|
+
|
|
10
30
|
## [2.78.3] - 2026-10-18
|
|
11
31
|
|
|
12
32
|
### Fixed
|
package/dist/browser-chats.js
CHANGED
|
@@ -30,7 +30,15 @@ export function parseHistoryMessages(messages) {
|
|
|
30
30
|
if (raw && typeof raw.accumulated_token_usage === 'number') {
|
|
31
31
|
usage = raw.accumulated_token_usage;
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
// Roles are matched case-insensitively, and a turn that is neither the
|
|
34
|
+
// operator nor the assistant (SYSTEM, TOOL, a future role) is DROPPED: the
|
|
35
|
+
// old `role === 'ASSISTANT'` fallback read every other turn as the
|
|
36
|
+
// operator's, so a lowercase assistant answer was printed as the operator's
|
|
37
|
+
// text in the restored dialogue.
|
|
38
|
+
const rawRole = String((raw && raw.role) || '').toUpperCase();
|
|
39
|
+
if (rawRole !== 'ASSISTANT' && rawRole !== 'USER')
|
|
40
|
+
continue;
|
|
41
|
+
const role = rawRole === 'ASSISTANT' ? 'assistant' : 'user';
|
|
34
42
|
const want = role === 'assistant' ? 'RESPONSE' : 'REQUEST';
|
|
35
43
|
let text = '';
|
|
36
44
|
for (const fr of (raw && raw.fragments) || []) {
|
package/dist/commands.js
CHANGED
|
@@ -544,15 +544,26 @@ export function isUsableCompactSummary(text) {
|
|
|
544
544
|
return false;
|
|
545
545
|
if (/^\(прервано пользователем\)$/.test(t))
|
|
546
546
|
return false;
|
|
547
|
+
// A tool call must be RECOGNIZED as one: a plain word `DSML` in prose (the
|
|
548
|
+
// summary describing the feature) is not noise. Requiring the tag/DSML
|
|
549
|
+
// syntax keeps the guard on real protocol output without rejecting a valid
|
|
550
|
+
// summary that merely mentions the feature (which made every retry fail and
|
|
551
|
+
// forced the local fallback).
|
|
547
552
|
if (parseAssistantToolCall(t))
|
|
548
553
|
return false;
|
|
549
|
-
|
|
554
|
+
// A REAL DSML call carries the marker punctuation (`<|DSML|invoke`); a
|
|
555
|
+
// summary may MENTION the feature by name in prose. Matching the bare word
|
|
556
|
+
// rejected a perfectly good summary and every retry failed the same way.
|
|
557
|
+
if (/<\s*[\uFF5C|]\s*DSML/i.test(t))
|
|
550
558
|
return false;
|
|
551
559
|
// A tool-call body without a recognizable head (a cut-off stream) still
|
|
552
560
|
// carries "args": { ... }.
|
|
553
561
|
if (/"args"\s*:\s*\{/.test(t))
|
|
554
562
|
return false;
|
|
555
|
-
|
|
563
|
+
// A REAL safety block is the marker, not a sentence that mentions it: the
|
|
564
|
+
// closing form `</ds_safety>` is what a summary would NOT write in prose
|
|
565
|
+
// (a summary may say "no <ds_safety> marker appeared").
|
|
566
|
+
if (/<\/ds_safety\s*>/i.test(t))
|
|
556
567
|
return false;
|
|
557
568
|
return true;
|
|
558
569
|
}
|
package/dist/config.js
CHANGED
|
@@ -527,6 +527,12 @@ function parseConfigValue(field, raw) {
|
|
|
527
527
|
throw new Error('boolean');
|
|
528
528
|
}
|
|
529
529
|
case 'number': {
|
|
530
|
+
// `Number('')` and `Number(' ')` are 0, NOT NaN: an empty value for a
|
|
531
|
+
// field with min 0 passed validation and silently BECAME 0 (an operator
|
|
532
|
+
// clearing the edit buffer turned retries/backups OFF instead of being
|
|
533
|
+
// told the value is invalid).
|
|
534
|
+
if (raw.trim() === '')
|
|
535
|
+
throw new Error('number');
|
|
530
536
|
const n = Number(raw);
|
|
531
537
|
if (!Number.isFinite(n))
|
|
532
538
|
throw new Error('number');
|
package/dist/extraTools.js
CHANGED
|
@@ -100,81 +100,119 @@ export function parsePatch(text) {
|
|
|
100
100
|
continue;
|
|
101
101
|
}
|
|
102
102
|
if (current)
|
|
103
|
-
current.lines.push(line);
|
|
103
|
+
current.lines.push(line.replace(/\r$/, ''));
|
|
104
104
|
}
|
|
105
105
|
if (!ops.length)
|
|
106
106
|
return { ops: [], error: 'Patch contains no file operations.' };
|
|
107
107
|
return { ops };
|
|
108
108
|
}
|
|
109
|
-
/**
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
* '+' (add). We locate the context block by exact match, then by ignoring
|
|
113
|
-
* trailing whitespace, then by ignoring all whitespace (like Codex).
|
|
114
|
-
*/
|
|
115
|
-
export function applyUpdateHunk(content, hunkLines) {
|
|
116
|
-
const hasCRLF = content.includes('\r\n');
|
|
117
|
-
const fileLines = content.split(/\r?\n/);
|
|
118
|
-
const oldLines = [];
|
|
119
|
-
const newLines = [];
|
|
109
|
+
/** Split a hunk list into per-`@@` blocks, remembering each header position. */
|
|
110
|
+
function splitHunks(hunkLines) {
|
|
111
|
+
const blocks = [{ oldStart: null, lines: [] }];
|
|
120
112
|
for (const l of hunkLines) {
|
|
121
|
-
if (l.startsWith('@@'))
|
|
113
|
+
if (l.startsWith('@@')) {
|
|
114
|
+
const m = l.match(/^@@\s*-(\d+)/);
|
|
115
|
+
blocks.push({ oldStart: m ? Number(m[1]) : null, lines: [] });
|
|
122
116
|
continue;
|
|
123
|
-
if (l.startsWith('+')) {
|
|
124
|
-
newLines.push(l.slice(1));
|
|
125
|
-
}
|
|
126
|
-
else if (l.startsWith('-')) {
|
|
127
|
-
oldLines.push(l.slice(1));
|
|
128
|
-
}
|
|
129
|
-
else if (l.startsWith(' ')) {
|
|
130
|
-
oldLines.push(l.slice(1));
|
|
131
|
-
newLines.push(l.slice(1));
|
|
132
|
-
}
|
|
133
|
-
else if (l === '') {
|
|
134
|
-
oldLines.push('');
|
|
135
|
-
newLines.push('');
|
|
136
117
|
}
|
|
137
|
-
|
|
138
|
-
oldLines.push(l);
|
|
139
|
-
newLines.push(l);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
if (!oldLines.length) {
|
|
143
|
-
return {
|
|
144
|
-
ok: false,
|
|
145
|
-
error: 'Hunk has no context or removed lines to anchor on.',
|
|
146
|
-
};
|
|
118
|
+
blocks[blocks.length - 1].lines.push(l);
|
|
147
119
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
120
|
+
return blocks.filter((b) => b.lines.length > 0);
|
|
121
|
+
}
|
|
122
|
+
const hunkNorm = (s, mode) => {
|
|
123
|
+
if (mode === 'exact')
|
|
124
|
+
return s;
|
|
125
|
+
if (mode === 'trim')
|
|
126
|
+
return s.replace(/[ \t]+$/, '');
|
|
127
|
+
return s.replace(/\s+/g, '');
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Find where a block's old side starts in `fileLines`.
|
|
131
|
+
* `from` is the lowest index allowed (hunks must not overlap / go backwards).
|
|
132
|
+
* When the header named a line, that position is preferred: repeated context
|
|
133
|
+
* is otherwise ambiguous and the wrong occurrence gets patched.
|
|
134
|
+
*/
|
|
135
|
+
function locateBlock(fileLines, oldLines, oldStart, from) {
|
|
136
|
+
const maxStart = fileLines.length - oldLines.length;
|
|
137
|
+
if (maxStart < from)
|
|
138
|
+
return -1;
|
|
155
139
|
for (const mode of ['exact', 'trim', 'ws']) {
|
|
156
|
-
const anchor = oldLines.map((l) =>
|
|
157
|
-
|
|
158
|
-
let match = true;
|
|
140
|
+
const anchor = oldLines.map((l) => hunkNorm(l, mode));
|
|
141
|
+
const matches = (i) => {
|
|
159
142
|
for (let j = 0; j < anchor.length; j++) {
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
break;
|
|
163
|
-
}
|
|
143
|
+
if (hunkNorm(fileLines[i + j], mode) !== anchor[j])
|
|
144
|
+
return false;
|
|
164
145
|
}
|
|
165
|
-
|
|
146
|
+
return true;
|
|
147
|
+
};
|
|
148
|
+
if (oldStart !== null) {
|
|
149
|
+
const hinted = oldStart - 1;
|
|
150
|
+
if (hinted >= from && hinted <= maxStart && matches(hinted))
|
|
151
|
+
return hinted;
|
|
152
|
+
}
|
|
153
|
+
for (let i = from; i <= maxStart; i++) {
|
|
154
|
+
if (matches(i))
|
|
155
|
+
return i;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return -1;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Apply an update hunk to existing content using context matching.
|
|
162
|
+
* The hunk is a list of lines prefixed with ' ' (context), '-' (remove),
|
|
163
|
+
* '+' (add). Several `@@` hunks for one file are applied in order, top to
|
|
164
|
+
* bottom. A block is located by exact match, then ignoring trailing
|
|
165
|
+
* whitespace, then ignoring all whitespace (like Codex).
|
|
166
|
+
*/
|
|
167
|
+
export function applyUpdateHunk(content, hunkLines) {
|
|
168
|
+
const hasCRLF = content.includes('\r\n');
|
|
169
|
+
const eol = hasCRLF ? '\r\n' : NL;
|
|
170
|
+
let fileLines = content.split(/\r?\n/);
|
|
171
|
+
for (const block of splitHunks(hunkLines)) {
|
|
172
|
+
const oldLines = [];
|
|
173
|
+
const newLines = [];
|
|
174
|
+
for (const l of block.lines) {
|
|
175
|
+
// A blank hunk line is the newline BEFORE "*** End Patch", not a
|
|
176
|
+
// context line: counting it as context demanded an extra blank file
|
|
177
|
+
// line and made every realistic multi-hunk patch fail.
|
|
178
|
+
if (l === '')
|
|
166
179
|
continue;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
180
|
+
if (l.startsWith('+')) {
|
|
181
|
+
newLines.push(l.slice(1));
|
|
182
|
+
}
|
|
183
|
+
else if (l.startsWith('-')) {
|
|
184
|
+
oldLines.push(l.slice(1));
|
|
185
|
+
}
|
|
186
|
+
else if (l.startsWith(' ')) {
|
|
187
|
+
oldLines.push(l.slice(1));
|
|
188
|
+
newLines.push(l.slice(1));
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
oldLines.push(l);
|
|
192
|
+
newLines.push(l);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (!oldLines.length) {
|
|
196
|
+
return {
|
|
197
|
+
ok: false,
|
|
198
|
+
error: 'Hunk has no context or removed lines to anchor on.',
|
|
199
|
+
};
|
|
171
200
|
}
|
|
201
|
+
const at = locateBlock(fileLines, oldLines, block.oldStart, 0);
|
|
202
|
+
if (at === -1) {
|
|
203
|
+
return {
|
|
204
|
+
ok: false,
|
|
205
|
+
error: 'Context not found in the file (hunk did not match): ' +
|
|
206
|
+
oldLines[0].slice(0, 60),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
fileLines = [
|
|
210
|
+
...fileLines.slice(0, at),
|
|
211
|
+
...newLines,
|
|
212
|
+
...fileLines.slice(at + oldLines.length),
|
|
213
|
+
];
|
|
172
214
|
}
|
|
173
|
-
return {
|
|
174
|
-
ok: false,
|
|
175
|
-
error: 'Context not found in the file (hunk did not match): ' +
|
|
176
|
-
oldLines[0].slice(0, 60),
|
|
177
|
-
};
|
|
215
|
+
return { ok: true, content: fileLines.join(eol) };
|
|
178
216
|
}
|
|
179
217
|
// ---------- factory ----------
|
|
180
218
|
export function createExtraTools(workdir, { undo, todos } = {}) {
|
package/dist/goal.js
CHANGED
|
@@ -86,11 +86,34 @@ export function parseGoalVerdict(text) {
|
|
|
86
86
|
// — the goal loop then auto-continued until the turn budget although the
|
|
87
87
|
// judge had already said NOT_MET. `MET` is tried LAST so it cannot match the
|
|
88
88
|
// tail of `NOT MET`.
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
//
|
|
90
|
+
// The evaluator sometimes ECHOES the requested label set before the real
|
|
91
|
+
// verdict (`VERDICT: MET | NOT_MET | IMPOSSIBLE`). Taking the FIRST match
|
|
92
|
+
// read the echo's `MET` as the verdict while REASON said the opposite, and
|
|
93
|
+
// MET CLEARS the goal — an unfinished task was silently accepted as done.
|
|
94
|
+
// A verdict line with a separator after the value is an echo, not a verdict;
|
|
95
|
+
// scan EVERY `VERDICT:` occurrence and take the first REAL one. The
|
|
96
|
+
// separator is checked on the character FOLLOWING the matched value (a
|
|
97
|
+
// lookahead INSIDE the value pattern was useless: the value alternatives
|
|
98
|
+
// always end on a word boundary of their own).
|
|
99
|
+
const label = /VERDICT\s*[*_`]*\s*:\s*[*_`]*\s*(IMPOSSIBLE|NOT[_ -]?MET|MET)/gi;
|
|
100
|
+
let m;
|
|
101
|
+
let status = null;
|
|
102
|
+
while ((m = label.exec(t)) !== null) {
|
|
103
|
+
// Skip whitespace/backticks between the value and whatever follows.
|
|
104
|
+
let k = m.index + m[0].length;
|
|
105
|
+
while (k < t.length && /[\s*_`]/.test(t.charAt(k)))
|
|
106
|
+
k++;
|
|
107
|
+
const after = t.charAt(k);
|
|
108
|
+
if (after === '|' || after === '/')
|
|
109
|
+
continue;
|
|
110
|
+
const raw = m[1].toUpperCase();
|
|
111
|
+
status =
|
|
112
|
+
raw === 'MET' ? 'met' : raw === 'IMPOSSIBLE' ? 'impossible' : 'not_met';
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
if (!status)
|
|
91
116
|
return null;
|
|
92
|
-
const raw = m[1].toUpperCase();
|
|
93
|
-
const status = raw === 'MET' ? 'met' : raw === 'IMPOSSIBLE' ? 'impossible' : 'not_met';
|
|
94
117
|
const rm = /REASON\s*[*_`]*\s*:\s*(.+)/i.exec(t);
|
|
95
118
|
const reason = rm
|
|
96
119
|
? rm[1]
|
package/dist/input/layout.js
CHANGED
|
@@ -59,15 +59,29 @@ export function pasteReplacement(raw) {
|
|
|
59
59
|
return null;
|
|
60
60
|
return { marker: formatPasteMarker(lines), text };
|
|
61
61
|
}
|
|
62
|
-
// Replace "[Pasted lines#N]" markers with the original pasted text
|
|
63
|
-
//
|
|
62
|
+
// Replace "[Pasted lines#N]" markers with the original pasted text. The marker
|
|
63
|
+
// carries ONLY the line count, so two pastes of the same length share it: a
|
|
64
|
+
// plain indexOf-per-paste substituted the FIRST paste's text into whichever
|
|
65
|
+
// marker survived (the operator deleted the first one) — the model then got
|
|
66
|
+
// the WRONG paste under a plausible marker. Replacing ONE occurrence per paste
|
|
67
|
+
// and tying each marker to the pasted text it was CREATED with fixes it for
|
|
68
|
+
// every order: an unmatched paste leaves the marker as text instead of
|
|
69
|
+
// swapping bodies.
|
|
64
70
|
export function expandPastes(pastes, text) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
if (!pastes.length)
|
|
72
|
+
return text;
|
|
73
|
+
// Expand in REVERSE order of appearance: each paste consumes its OWN marker
|
|
74
|
+
// occurrence (the last one for the last paste), so a marker the operator
|
|
75
|
+
// deleted cannot shift another paste's text into the wrong slot. With a
|
|
76
|
+
// single occurrence left, the LAST paste owns it — the operator deleted the
|
|
77
|
+
// EARLIER marker, so the later paste's body is the right fill.
|
|
78
|
+
let out = String(text);
|
|
79
|
+
for (let i = pastes.length - 1; i >= 0; i--) {
|
|
80
|
+
const p = pastes[i];
|
|
81
|
+
const idx = out.lastIndexOf(p.marker);
|
|
82
|
+
if (idx === -1)
|
|
83
|
+
continue;
|
|
84
|
+
out = out.slice(0, idx) + p.text + out.slice(idx + p.marker.length);
|
|
71
85
|
}
|
|
72
86
|
return out;
|
|
73
87
|
}
|
package/dist/permissions.js
CHANGED
|
@@ -80,9 +80,22 @@ function looksLikePath(v) {
|
|
|
80
80
|
export function collectPathCandidates(tool, args) {
|
|
81
81
|
const a = (args ?? {});
|
|
82
82
|
const out = [];
|
|
83
|
+
// A path may arrive as a STRING or as an ARRAY of strings (`files: [...]`,
|
|
84
|
+
// `path: [...]`), and MCP tools nest their args (`{file: {path}}`). Accepting
|
|
85
|
+
// only a top-level string let a `path` deny rule be bypassed by wrapping the
|
|
86
|
+
// same path one level deeper or in an array.
|
|
87
|
+
const addValue = (v) => {
|
|
88
|
+
if (typeof v === 'string')
|
|
89
|
+
out.push(v);
|
|
90
|
+
else if (Array.isArray(v)) {
|
|
91
|
+
for (const x of v)
|
|
92
|
+
if (typeof x === 'string')
|
|
93
|
+
out.push(x);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
83
96
|
for (const k of PATH_KEYS) {
|
|
84
|
-
if (
|
|
85
|
-
|
|
97
|
+
if (k in a)
|
|
98
|
+
addValue(a[k]);
|
|
86
99
|
}
|
|
87
100
|
if (tool === 'ApplyPatch') {
|
|
88
101
|
// Parse the patch and pull the file headers out of it.
|
|
@@ -96,13 +109,33 @@ export function collectPathCandidates(tool, args) {
|
|
|
96
109
|
while ((m = re.exec(text)))
|
|
97
110
|
out.push(m[1]);
|
|
98
111
|
}
|
|
99
|
-
// MCP tools (and any unknown tool): scan the
|
|
100
|
-
// entries
|
|
112
|
+
// MCP tools (and any unknown tool): scan the values for path-looking
|
|
113
|
+
// entries — through ARRAYS and NESTED objects too, since an MCP schema is
|
|
114
|
+
// free to nest (`{file: {path}}`) or take `argv: ['cat', '/etc/x']`. Only
|
|
115
|
+
// values that clearly look like paths are taken (depth-bounded to avoid a
|
|
116
|
+
// pathological payload).
|
|
101
117
|
if (tool.includes('__')) {
|
|
102
|
-
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
118
|
+
const walk = (v, depth) => {
|
|
119
|
+
if (depth > 4 || v === null || v === undefined)
|
|
120
|
+
return;
|
|
121
|
+
if (typeof v === 'string') {
|
|
122
|
+
if (looksLikePath(v))
|
|
123
|
+
out.push(v);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (Array.isArray(v)) {
|
|
127
|
+
for (const x of v)
|
|
128
|
+
walk(x, depth + 1);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (typeof v === 'object') {
|
|
132
|
+
for (const x of Object.values(v)) {
|
|
133
|
+
walk(x, depth + 1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
for (const v of Object.values(a))
|
|
138
|
+
walk(v, 0);
|
|
106
139
|
}
|
|
107
140
|
return out;
|
|
108
141
|
}
|
package/dist/qwen-chats.js
CHANGED
|
@@ -101,18 +101,33 @@ export function parseQwenChatList(json) {
|
|
|
101
101
|
data.list ??
|
|
102
102
|
data.items)
|
|
103
103
|
: null;
|
|
104
|
-
|
|
104
|
+
// The payload has also been seen as a MAP keyed by chat id (the same
|
|
105
|
+
// convention as the history shape); the key then IS the id. Without this a
|
|
106
|
+
// legitimate list was rejected by the array guard and /chats printed
|
|
107
|
+
// "(none)" with no error.
|
|
108
|
+
const keyed = Array.isArray(arr)
|
|
109
|
+
? null
|
|
110
|
+
: arr && typeof arr === 'object'
|
|
111
|
+
? Object.entries(arr)
|
|
112
|
+
: null;
|
|
113
|
+
if (!Array.isArray(arr) && !keyed)
|
|
105
114
|
return [];
|
|
115
|
+
const entries = Array.isArray(arr)
|
|
116
|
+
? arr.map((c) => ['', c])
|
|
117
|
+
: keyed;
|
|
106
118
|
const out = [];
|
|
107
|
-
for (const c of
|
|
119
|
+
for (const [key, c] of entries) {
|
|
108
120
|
if (!c || typeof c !== 'object')
|
|
109
121
|
continue;
|
|
110
122
|
const m = c;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
123
|
+
// A numeric id (`{id: 12345}`) used to be dropped as "no id"; it is a
|
|
124
|
+
// perfectly usable chat id once stringified.
|
|
125
|
+
const raw = m.id ?? m.chat_id;
|
|
126
|
+
const id = typeof raw === 'string'
|
|
127
|
+
? raw
|
|
128
|
+
: typeof raw === 'number' && Number.isFinite(raw)
|
|
129
|
+
? String(raw)
|
|
130
|
+
: key;
|
|
116
131
|
if (!id)
|
|
117
132
|
continue;
|
|
118
133
|
const title = (typeof m.title === 'string' && m.title) ||
|
|
@@ -163,14 +178,23 @@ export function parseQwenHistory(json) {
|
|
|
163
178
|
const out = [];
|
|
164
179
|
for (const m of sorted) {
|
|
165
180
|
// Role detection is DEFENSIVE: the field has been `role` (string,
|
|
166
|
-
// case-insensitive), a
|
|
181
|
+
// case-insensitive), a NUMERIC flag, or `is_user`. Anything that is not
|
|
167
182
|
// clearly the user is treated as the assistant (the restored dialogue
|
|
168
|
-
// cares about the distinction, not the exact enum).
|
|
183
|
+
// cares about the distinction, not the exact enum). A numeric flag uses
|
|
184
|
+
// 1 = user, 2 = assistant (the `String(1) === '1'` form was ignored and
|
|
185
|
+
// every user turn was attributed to the model).
|
|
169
186
|
const roleStr = String(m.role ?? '').toLowerCase();
|
|
187
|
+
// `from` is matched case-insensitively and accepts Qwen's `human`
|
|
188
|
+
// spelling: the exact `from === 'user'` test read an uppercase/human
|
|
189
|
+
// turn as the ASSISTANT's answer, so the operator's own question was
|
|
190
|
+
// restored as the model's reply.
|
|
191
|
+
const fromStr = String(m.from ?? '').toLowerCase();
|
|
170
192
|
const isUser = roleStr === 'user' ||
|
|
193
|
+
roleStr === '1' ||
|
|
171
194
|
roleStr === '0' ||
|
|
172
195
|
m.is_user === true ||
|
|
173
|
-
|
|
196
|
+
fromStr === 'user' ||
|
|
197
|
+
fromStr === 'human';
|
|
174
198
|
const role = isUser ? 'user' : 'assistant';
|
|
175
199
|
let text = '';
|
|
176
200
|
if (typeof m.content === 'string')
|
package/dist/qwen-net-capture.js
CHANGED
|
@@ -184,7 +184,12 @@ export function isFinishedWithoutAnswer(body) {
|
|
|
184
184
|
for (const ch of choices) {
|
|
185
185
|
if (!ch || typeof ch !== 'object')
|
|
186
186
|
continue;
|
|
187
|
-
const
|
|
187
|
+
const c = ch;
|
|
188
|
+
// The final chunk carries `status` on the CHOICE (see the format comment
|
|
189
|
+
// at the top of this file); older/other builds put it on `delta`.
|
|
190
|
+
if (c.status === 'finished')
|
|
191
|
+
finished = true;
|
|
192
|
+
const delta = c.delta;
|
|
188
193
|
if (delta && delta.status === 'finished')
|
|
189
194
|
finished = true;
|
|
190
195
|
}
|
package/dist/scheduler.js
CHANGED
|
@@ -56,7 +56,13 @@ function dayMatches(c, date) {
|
|
|
56
56
|
function parseCronField(field, min, max) {
|
|
57
57
|
const out = new Set();
|
|
58
58
|
for (const part of field.split(',')) {
|
|
59
|
-
|
|
59
|
+
// An empty list item (`1,,2`, a trailing comma) used to fall through
|
|
60
|
+
// Number('') === 0 and silently ADD minute 0 to the schedule.
|
|
61
|
+
if (!part)
|
|
62
|
+
return null;
|
|
63
|
+
const [range, stepStr, extra] = part.split('/');
|
|
64
|
+
if (extra !== undefined)
|
|
65
|
+
return null;
|
|
60
66
|
const step = stepStr === undefined ? 1 : Number(stepStr);
|
|
61
67
|
if (!Number.isInteger(step) || step <= 0)
|
|
62
68
|
return null;
|
|
@@ -67,11 +73,22 @@ function parseCronField(field, min, max) {
|
|
|
67
73
|
hi = max;
|
|
68
74
|
}
|
|
69
75
|
else if (range.includes('-')) {
|
|
70
|
-
|
|
76
|
+
// A bare number is required on BOTH sides: `split('-')` on `-5` gave
|
|
77
|
+
// ['', '5'] and Number('') === 0, so a typo `-5 * * * *` became the
|
|
78
|
+
// range 0-5 instead of being rejected. `1-2-3` was silently truncated
|
|
79
|
+
// to 1-2 the same way.
|
|
80
|
+
const segs = range.split('-');
|
|
81
|
+
if (segs.length !== 2)
|
|
82
|
+
return null;
|
|
83
|
+
const [a, b] = segs;
|
|
84
|
+
if (!/^\d+$/.test(a) || !/^\d+$/.test(b))
|
|
85
|
+
return null;
|
|
71
86
|
lo = Number(a);
|
|
72
87
|
hi = Number(b);
|
|
73
88
|
}
|
|
74
89
|
else {
|
|
90
|
+
if (!/^\d+$/.test(range))
|
|
91
|
+
return null;
|
|
75
92
|
lo = Number(range);
|
|
76
93
|
// `N/step` (a start with a step, no range) is valid cron: it means from
|
|
77
94
|
// N up to the field maximum. Treating it as the single value N made a
|
package/dist/web.js
CHANGED
|
@@ -226,6 +226,28 @@ export function isPrivateIp(ip) {
|
|
|
226
226
|
// ff00::/8 multicast is not a public unicast destination either.
|
|
227
227
|
if ((g0 & 0xff00) === 0xff00)
|
|
228
228
|
return true;
|
|
229
|
+
// 6to4 (2002:AABB:CCDD::/48) embeds an IPv4 in groups 1-2, so
|
|
230
|
+
// `2002:7f00:0001::` IS 127.0.0.1 and `2002:a9fe:a9fe::` is the cloud
|
|
231
|
+
// metadata address. Judged by the embedded address, like the other
|
|
232
|
+
// transition prefixes.
|
|
233
|
+
if (v6[0] === 0x2002) {
|
|
234
|
+
return isPrivateIp((v6[1] >> 8) +
|
|
235
|
+
'.' +
|
|
236
|
+
(v6[1] & 0xff) +
|
|
237
|
+
'.' +
|
|
238
|
+
(v6[2] >> 8) +
|
|
239
|
+
'.' +
|
|
240
|
+
(v6[2] & 0xff));
|
|
241
|
+
}
|
|
242
|
+
// Teredo (2001:0000::/32): the CLIENT IPv4 is the last 32 bits XORed with
|
|
243
|
+
// 0xffffffff (the server IPv4 sits in groups 3-4 and is not the peer).
|
|
244
|
+
if (v6[0] === 0x2001 && v6[1] === 0x0000) {
|
|
245
|
+
const a = (v6[6] >> 8) ^ 0xff;
|
|
246
|
+
const b = (v6[6] & 0xff) ^ 0xff;
|
|
247
|
+
const c = (v6[7] >> 8) ^ 0xff;
|
|
248
|
+
const d = (v6[7] & 0xff) ^ 0xff;
|
|
249
|
+
return isPrivateIp(a + '.' + b + '.' + c + '.' + d);
|
|
250
|
+
}
|
|
229
251
|
// IPv4-mapped ::ffff:a.b.c.d — judge the embedded address.
|
|
230
252
|
const mappedV4 = v6.slice(0, 5).every((g) => g === 0) && v6[5] === 0xffff;
|
|
231
253
|
if (mappedV4) {
|
|
@@ -625,7 +647,11 @@ export function parseDuckDuckGoResults(body, limit) {
|
|
|
625
647
|
href = decodeURIComponent(uddgMatch[1]);
|
|
626
648
|
const title = htmlToText(m[2] || '').trim();
|
|
627
649
|
const snippet = htmlToText(m[3] || '').trim();
|
|
628
|
-
|
|
650
|
+
// Only an absolute http(s) link is a real result. The page can carry a
|
|
651
|
+
// `javascript:`/`data:` href (malicious or broken markup) or a
|
|
652
|
+
// site-relative one; handing either to the model as a result URL is wrong
|
|
653
|
+
// (a `javascript:` URL looks fetchable, a relative one is not).
|
|
654
|
+
if (!title || !/^https?:\/\//i.test(href))
|
|
629
655
|
continue;
|
|
630
656
|
results.push({ title, url: href, snippet });
|
|
631
657
|
}
|
package/dist/xml-toolcall.js
CHANGED
|
@@ -46,13 +46,29 @@ function readAllAttrs(attrs) {
|
|
|
46
46
|
// (the agent then stalls). Here we look for the first JSON object inside the
|
|
47
47
|
// attributes/body and parse it as args.
|
|
48
48
|
function parseInlineJsonArgs(s) {
|
|
49
|
+
// Find the start of the inline JSON. The SPECIFIC `"args" {` marker wins:
|
|
50
|
+
// a generic `{` in the surrounding prose (`note {here}: {…}`) was taken as
|
|
51
|
+
// the object start, balance ended on its `}`, and the call reached the tool
|
|
52
|
+
// with EMPTY args. Fall back to a bare `{` only when the marker is absent.
|
|
53
|
+
const argsMarker = /["']args["']\s*[:=]?\s*\{/i.exec(s);
|
|
54
|
+
if (argsMarker)
|
|
55
|
+
return parseBalancedJson(s, argsMarker.index + argsMarker[0].length - 1);
|
|
56
|
+
// No explicit marker: try EVERY `{` and take the first one that actually
|
|
57
|
+
// parses as an object. Picking the first `{` blindly broke on a stray brace
|
|
58
|
+
// in the prose (`note {here}: {…}`): balance ended on its `}`, the fragment
|
|
59
|
+
// carried no arguments, and the tool ran with EMPTY args.
|
|
60
|
+
const re = /\{/g;
|
|
61
|
+
let b;
|
|
62
|
+
while ((b = re.exec(s)) !== null) {
|
|
63
|
+
const parsed = parseBalancedJson(s, b.index);
|
|
64
|
+
if (parsed)
|
|
65
|
+
return parsed;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
/** Parse one balanced, string-aware `{...}` object that STARTS at `from`. */
|
|
70
|
+
function parseBalancedJson(s, from) {
|
|
49
71
|
const Q = String.fromCharCode(34);
|
|
50
|
-
// Find a plausible start of a JSON object: `{` or `"args" {`/`'args' {`.
|
|
51
|
-
const startRe = /\{|["']args["']\s*[:=]?\s*\{/i;
|
|
52
|
-
const m = startRe.exec(s);
|
|
53
|
-
if (!m)
|
|
54
|
-
return null;
|
|
55
|
-
const from = m.index;
|
|
56
72
|
// Scan for a balanced {...} block (string-aware) and try to parse it.
|
|
57
73
|
let depth = 0;
|
|
58
74
|
let inStr = false;
|
|
@@ -112,14 +128,47 @@ function parseInlineJsonArgs(s) {
|
|
|
112
128
|
// parameter).
|
|
113
129
|
function parseParameters(body) {
|
|
114
130
|
const args = {};
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
131
|
+
// The VALUE is arbitrary model text: a Write/Edit content may legitimately
|
|
132
|
+
// contain the literal `</parameter>` (writing an XML parser, a docs
|
|
133
|
+
// example). A plain non-greedy scan stops at that FIRST close tag and the
|
|
134
|
+
// rest of the content is silently lost — the file is written TRUNCATED.
|
|
135
|
+
// So: find every opening tag, then take as the value everything up to the
|
|
136
|
+
// LAST close tag of the same name that precedes the next opening tag (or the
|
|
137
|
+
// end of the body). A stray `</parameter>` inside a value is thus kept.
|
|
138
|
+
// An opening tag is `<parameter …>` / `<|DSML|parameter …>` — NOT the
|
|
139
|
+
// closing `</parameter>`, whose name ALSO matches this pattern (the prefix
|
|
140
|
+
// before `parameter` is not required to be empty). Treating a close tag as
|
|
141
|
+
// an open one shifted every limit by one element and cut the last value;
|
|
142
|
+
// a slash directly before `parameter` marks the close form.
|
|
143
|
+
const openRe = /<[^>]*?(?<!\/)parameter([^>]*)>/gi;
|
|
144
|
+
const opens = [];
|
|
145
|
+
let m;
|
|
146
|
+
while ((m = openRe.exec(body)) !== null) {
|
|
147
|
+
opens.push({
|
|
148
|
+
attrs: m[1] || '',
|
|
149
|
+
start: m.index,
|
|
150
|
+
valueStart: m.index + m[0].length,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const closeRe = /<\/[^>]*?parameter[^>]*>/gi;
|
|
154
|
+
for (let i = 0; i < opens.length; i++) {
|
|
155
|
+
const o = opens[i];
|
|
156
|
+
const pname = readAttr(o.attrs, 'name');
|
|
119
157
|
if (!pname)
|
|
120
158
|
continue;
|
|
121
|
-
const
|
|
122
|
-
|
|
159
|
+
const limit = i + 1 < opens.length ? opens[i + 1].start : body.length;
|
|
160
|
+
// The LAST close tag before the next opening tag (or the end).
|
|
161
|
+
let valueEnd = limit;
|
|
162
|
+
closeRe.lastIndex = o.valueStart;
|
|
163
|
+
let c;
|
|
164
|
+
while ((c = closeRe.exec(body)) !== null) {
|
|
165
|
+
if (c.index >= limit)
|
|
166
|
+
break;
|
|
167
|
+
valueEnd = c.index;
|
|
168
|
+
}
|
|
169
|
+
const raw = body.slice(o.valueStart, valueEnd);
|
|
170
|
+
const isString = /string[ ]*=[ ]*"?true"?/i.test(o.attrs);
|
|
171
|
+
let value = unescapeXml(raw);
|
|
123
172
|
if (!isString) {
|
|
124
173
|
try {
|
|
125
174
|
value = JSON.parse(value);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.79.0",
|
|
4
4
|
"description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen and other web LLMs — no API key required. Reads and edits files, runs commands, commits to git, like Claude Code / Codex CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|