@yeaft/webchat-agent 0.1.863 → 0.1.865
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/connection/message-router.js +5 -1
- package/history.js +29 -0
- package/package.json +1 -1
- package/providers/copilot-models.js +126 -12
- package/providers/copilot.js +7 -4
- package/yeaft/web-bridge.js +54 -22
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
handleGitStatus, handleGitDiff, handleGitAdd, handleGitReset, handleGitRestore, handleGitCommit, handleGitPush,
|
|
19
19
|
handleFileSearch, handleCreateFile, handleDeleteFiles, handleMoveFiles, handleCopyFiles, handleUploadToDir, handleTransferFiles
|
|
20
20
|
} from '../workbench.js';
|
|
21
|
-
import { handleListHistorySessions, handleListFolders } from '../history.js';
|
|
21
|
+
import { handleListHistorySessions, handleListFolders, handleListModels } from '../history.js';
|
|
22
22
|
import {
|
|
23
23
|
createConversation, resumeConversation, deleteConversation,
|
|
24
24
|
handleRefreshConversation, handleCancelExecution,
|
|
@@ -112,6 +112,10 @@ export async function handleMessage(msg) {
|
|
|
112
112
|
await handleListFolders(msg);
|
|
113
113
|
break;
|
|
114
114
|
|
|
115
|
+
case 'list_models':
|
|
116
|
+
await handleListModels(msg);
|
|
117
|
+
break;
|
|
118
|
+
|
|
115
119
|
case 'transfer_files':
|
|
116
120
|
await handleTransferFiles(msg);
|
|
117
121
|
break;
|
package/history.js
CHANGED
|
@@ -252,3 +252,32 @@ export async function handleListFolders(msg) {
|
|
|
252
252
|
});
|
|
253
253
|
}
|
|
254
254
|
}
|
|
255
|
+
|
|
256
|
+
// 列出指定 provider 下可选 model
|
|
257
|
+
export async function handleListModels(msg) {
|
|
258
|
+
const { requestId, _requestClientId, provider } = msg;
|
|
259
|
+
const providerName = provider || DEFAULT_PROVIDER;
|
|
260
|
+
try {
|
|
261
|
+
const driver = getProvider(providerName);
|
|
262
|
+
let models = [];
|
|
263
|
+
if (typeof driver.listModels === 'function') {
|
|
264
|
+
models = await driver.listModels();
|
|
265
|
+
}
|
|
266
|
+
ctx.sendToServer({
|
|
267
|
+
type: 'models_list',
|
|
268
|
+
requestId,
|
|
269
|
+
_requestClientId,
|
|
270
|
+
provider: providerName,
|
|
271
|
+
models: Array.isArray(models) ? models : [],
|
|
272
|
+
});
|
|
273
|
+
} catch (e) {
|
|
274
|
+
ctx.sendToServer({
|
|
275
|
+
type: 'models_list',
|
|
276
|
+
requestId,
|
|
277
|
+
_requestClientId,
|
|
278
|
+
provider: providerName,
|
|
279
|
+
models: [],
|
|
280
|
+
error: e.message,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,131 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Copilot model list.
|
|
3
|
+
*
|
|
4
|
+
* The Copilot CLI accepts `--model <id>` and the agent-facing API exposes
|
|
5
|
+
* `GET https://api.githubcopilot.com/models` — the same endpoint VS Code's
|
|
6
|
+
* model picker uses. We hit it on demand (cached in-process for 10 min),
|
|
7
|
+
* filter to picker-enabled chat models, and fall back to a curated static
|
|
8
|
+
* list if the network call fails (so the UI never shows an empty picker).
|
|
9
|
+
*
|
|
10
|
+
* Auth re-uses the existing `agent/yeaft/llm/credentials/github-copilot.js`
|
|
11
|
+
* credential pipeline — gh CLI / env / persisted OAuth all work; the user
|
|
12
|
+
* does not need to paste an API key as long as Copilot CLI is logged in.
|
|
6
13
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
import { getApiToken, copilotRequestHeaders, validateRawToken, resolveRawToken } from '../yeaft/llm/credentials/github-copilot.js';
|
|
16
|
+
import { readFile } from 'fs/promises';
|
|
17
|
+
import { homedir } from 'os';
|
|
18
|
+
import { join } from 'path';
|
|
19
|
+
|
|
20
|
+
// Curated fallback — small, but covers the common picks if /models is down or
|
|
21
|
+
// the user has no Copilot auth available yet.
|
|
22
|
+
export const FALLBACK_COPILOT_MODELS = Object.freeze([
|
|
23
|
+
{ id: 'claude-sonnet-4.5', label: 'Claude Sonnet 4.5', vendor: 'Anthropic' },
|
|
24
|
+
{ id: 'claude-sonnet-4', label: 'Claude Sonnet 4', vendor: 'Anthropic' },
|
|
25
|
+
{ id: 'claude-opus-4.1', label: 'Claude Opus 4.1', vendor: 'Anthropic' },
|
|
26
|
+
{ id: 'gpt-5', label: 'GPT-5', vendor: 'OpenAI' },
|
|
27
|
+
{ id: 'gpt-5-mini', label: 'GPT-5 Mini', vendor: 'OpenAI' },
|
|
28
|
+
{ id: 'gpt-4.1', label: 'GPT-4.1', vendor: 'OpenAI' },
|
|
29
|
+
{ id: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro', vendor: 'Google' },
|
|
15
30
|
]);
|
|
16
31
|
|
|
17
32
|
export const DEFAULT_COPILOT_MODEL = 'claude-sonnet-4.5';
|
|
33
|
+
|
|
34
|
+
const MODELS_ENDPOINT = 'https://api.githubcopilot.com/models';
|
|
35
|
+
const CACHE_TTL_MS = 10 * 60 * 1000;
|
|
36
|
+
const COPILOT_CLI_CONFIG = join(homedir(), '.copilot', 'config.json');
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Last-resort token source: the Copilot CLI itself caches an OAuth token at
|
|
40
|
+
* ~/.copilot/config.json after `copilot login`. If the standard yeaft
|
|
41
|
+
* credential pipeline (env / gh CLI / persisted device flow) didn't surface
|
|
42
|
+
* a token, we re-use the CLI's. Same auth surface — no extra perm needed.
|
|
43
|
+
*/
|
|
44
|
+
async function _resolveCopilotCliToken() {
|
|
45
|
+
try {
|
|
46
|
+
const raw = await readFile(COPILOT_CLI_CONFIG, 'utf8');
|
|
47
|
+
// Copilot CLI's config.json starts with `//` comments. Strip line comments
|
|
48
|
+
// before JSON.parse — the file otherwise has no string-context `//`.
|
|
49
|
+
const cleaned = raw.split('\n').filter(l => !l.trim().startsWith('//')).join('\n');
|
|
50
|
+
const cfg = JSON.parse(cleaned);
|
|
51
|
+
const tokens = cfg?.copilotTokens && typeof cfg.copilotTokens === 'object' ? cfg.copilotTokens : null;
|
|
52
|
+
if (!tokens) return null;
|
|
53
|
+
for (const tok of Object.values(tokens)) {
|
|
54
|
+
if (typeof tok === 'string' && validateRawToken(tok).valid) return tok;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function _resolveBearerToken() {
|
|
63
|
+
// The /models endpoint accepts the GitHub OAuth token directly as a Bearer
|
|
64
|
+
// — no token exchange needed (unlike the chat-completion endpoints). So we
|
|
65
|
+
// prefer the raw OAuth from the standard yeaft pipeline first, then fall
|
|
66
|
+
// back to the Copilot CLI's own cached OAuth at ~/.copilot/config.json.
|
|
67
|
+
try {
|
|
68
|
+
const raw = await resolveRawToken();
|
|
69
|
+
if (raw?.token) return raw.token;
|
|
70
|
+
} catch { /* fall through */ }
|
|
71
|
+
const cliRaw = await _resolveCopilotCliToken();
|
|
72
|
+
if (cliRaw) return cliRaw;
|
|
73
|
+
// Last resort: try exchanged token (works for chat endpoints; may also work
|
|
74
|
+
// if /models gained that auth path in future).
|
|
75
|
+
const cred = await getApiToken();
|
|
76
|
+
return cred?.token || null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let _cache = null; // { models, fetchedAt }
|
|
80
|
+
let _inflight = null; // dedupes concurrent cold-cache calls
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Returns a list of `{id, label, vendor, preview, family}` records,
|
|
84
|
+
* picker-enabled chat models only. Cached for 10 minutes. Never throws —
|
|
85
|
+
* falls back to the static list on any error (including network timeout).
|
|
86
|
+
*/
|
|
87
|
+
export async function listCopilotModels({ force = false } = {}) {
|
|
88
|
+
if (!force && _cache && Date.now() - _cache.fetchedAt < CACHE_TTL_MS) {
|
|
89
|
+
return _cache.models.slice();
|
|
90
|
+
}
|
|
91
|
+
if (_inflight) return (await _inflight).slice();
|
|
92
|
+
_inflight = (async () => {
|
|
93
|
+
const ac = new AbortController();
|
|
94
|
+
const timer = setTimeout(() => ac.abort(), 5000);
|
|
95
|
+
try {
|
|
96
|
+
const token = await _resolveBearerToken();
|
|
97
|
+
if (!token) return FALLBACK_COPILOT_MODELS.slice();
|
|
98
|
+
const res = await fetch(MODELS_ENDPOINT, {
|
|
99
|
+
signal: ac.signal,
|
|
100
|
+
headers: { ...copilotRequestHeaders({ isAgentTurn: false }), Authorization: `Bearer ${token}` },
|
|
101
|
+
});
|
|
102
|
+
if (!res.ok) return FALLBACK_COPILOT_MODELS.slice();
|
|
103
|
+
const body = await res.json();
|
|
104
|
+
const data = Array.isArray(body?.data) ? body.data : [];
|
|
105
|
+
const models = data
|
|
106
|
+
.filter(m => m && m.model_picker_enabled && m.capabilities?.type === 'chat')
|
|
107
|
+
.map(m => ({
|
|
108
|
+
id: m.id,
|
|
109
|
+
label: m.name || m.id,
|
|
110
|
+
vendor: m.vendor || '',
|
|
111
|
+
preview: !!m.preview,
|
|
112
|
+
family: m.capabilities?.family || '',
|
|
113
|
+
}));
|
|
114
|
+
if (!models.length) return FALLBACK_COPILOT_MODELS.slice();
|
|
115
|
+
_cache = { models, fetchedAt: Date.now() };
|
|
116
|
+
return models.slice();
|
|
117
|
+
} catch {
|
|
118
|
+
return FALLBACK_COPILOT_MODELS.slice();
|
|
119
|
+
} finally {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
}
|
|
122
|
+
})();
|
|
123
|
+
try {
|
|
124
|
+
return (await _inflight).slice();
|
|
125
|
+
} finally {
|
|
126
|
+
_inflight = null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Tests only. */
|
|
131
|
+
export function _resetCopilotModelsCacheForTests() { _cache = null; _inflight = null; }
|
package/providers/copilot.js
CHANGED
|
@@ -6,7 +6,7 @@ import { join } from 'path';
|
|
|
6
6
|
import { DatabaseSync } from 'node:sqlite';
|
|
7
7
|
import ctx from '../context.js';
|
|
8
8
|
import { AcpClient } from './acp-client.js';
|
|
9
|
-
import {
|
|
9
|
+
import { listCopilotModels, DEFAULT_COPILOT_MODEL } from './copilot-models.js';
|
|
10
10
|
|
|
11
11
|
export const name = 'copilot';
|
|
12
12
|
|
|
@@ -582,9 +582,12 @@ function _knownCopilotTools() {
|
|
|
582
582
|
];
|
|
583
583
|
}
|
|
584
584
|
|
|
585
|
-
/**
|
|
586
|
-
|
|
587
|
-
|
|
585
|
+
/**
|
|
586
|
+
* Exported for the model picker UI. Async — hits Copilot's /models endpoint
|
|
587
|
+
* (cached) and falls back to a static list if auth/network fails.
|
|
588
|
+
*/
|
|
589
|
+
export async function listModels() {
|
|
590
|
+
return await listCopilotModels();
|
|
588
591
|
}
|
|
589
592
|
|
|
590
593
|
export default { name, capabilities, start, sendInput, abort, clear, listFolders, listSessions, loadHistory, listModels, respondToPermissionRequest };
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -1717,6 +1717,32 @@ function chatErrorPayload(err) {
|
|
|
1717
1717
|
};
|
|
1718
1718
|
}
|
|
1719
1719
|
|
|
1720
|
+
/**
|
|
1721
|
+
* Build the vpPersona payload threaded into engine.query so the worker
|
|
1722
|
+
* system prompt carries the VP's identity/role/persona/planInstruction.
|
|
1723
|
+
* Returns null on miss — callers treat that as "use generic prompt".
|
|
1724
|
+
* Shared by handleYeaftChatSend and the group fan-out path so the field
|
|
1725
|
+
* set stays in lockstep.
|
|
1726
|
+
*/
|
|
1727
|
+
function buildVpPersona(vpId) {
|
|
1728
|
+
if (!vpId) return null;
|
|
1729
|
+
try {
|
|
1730
|
+
const vp = readVp(vpId);
|
|
1731
|
+
if (!vp) return null;
|
|
1732
|
+
return {
|
|
1733
|
+
vpId,
|
|
1734
|
+
displayName: vp.displayName || vpId,
|
|
1735
|
+
displayNameZh: vp.displayNameZh || '',
|
|
1736
|
+
role: vp.role || '',
|
|
1737
|
+
roleZh: vp.roleZh || '',
|
|
1738
|
+
persona: vp.persona || '',
|
|
1739
|
+
planInstruction: typeof vp.planInstruction === 'string' ? vp.planInstruction : '',
|
|
1740
|
+
};
|
|
1741
|
+
} catch {
|
|
1742
|
+
return null;
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1720
1746
|
export function handleYeaftListChats(msg) {
|
|
1721
1747
|
const requestId = msg && msg.requestId;
|
|
1722
1748
|
try {
|
|
@@ -1729,19 +1755,34 @@ export function handleYeaftListChats(msg) {
|
|
|
1729
1755
|
|
|
1730
1756
|
export function handleYeaftCreateChat(msg) {
|
|
1731
1757
|
const requestId = msg && msg.requestId;
|
|
1758
|
+
// Accept fields at top-level (current wire shape) AND inside `payload`
|
|
1759
|
+
// (legacy / future) — keeps backwards compatibility cheap.
|
|
1760
|
+
const top = msg || {};
|
|
1732
1761
|
const payload = (msg && msg.payload) || {};
|
|
1762
|
+
const displayName = payload.displayName || top.displayName;
|
|
1763
|
+
const workDir = payload.workDir || top.workDir;
|
|
1764
|
+
const explicitId = payload.id || top.id;
|
|
1765
|
+
// Chat mode is 1:1 with the built-in Omni assistant by default. The
|
|
1766
|
+
// VP picker is gone from the UI; callers may still override vpId to
|
|
1767
|
+
// bind a chat to a specialist, but the default is omni.
|
|
1768
|
+
const vpId = payload.vpId || top.vpId || 'omni';
|
|
1733
1769
|
try {
|
|
1734
1770
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
1735
1771
|
if (!yeaftDir) throw new Error('no yeaft directory configured');
|
|
1736
|
-
if
|
|
1772
|
+
// Fail loud at create time if the requested VP (default: omni) is
|
|
1773
|
+
// not installed — otherwise the chat would silently degrade to a
|
|
1774
|
+
// generic prompt at first send.
|
|
1775
|
+
if (!buildVpPersona(vpId)) {
|
|
1776
|
+
throw new Error(`VP '${vpId}' is not installed`);
|
|
1777
|
+
}
|
|
1737
1778
|
const root = chatsRootFor(yeaftDir);
|
|
1738
|
-
const chatId = (
|
|
1779
|
+
const chatId = (explicitId && String(explicitId).trim())
|
|
1739
1780
|
|| `chat_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1740
1781
|
const h = createChatStore(root, {
|
|
1741
1782
|
id: chatId,
|
|
1742
|
-
vpId
|
|
1743
|
-
displayName
|
|
1744
|
-
workDir
|
|
1783
|
+
vpId,
|
|
1784
|
+
displayName,
|
|
1785
|
+
workDir,
|
|
1745
1786
|
});
|
|
1746
1787
|
const meta = h.getMeta();
|
|
1747
1788
|
h.close();
|
|
@@ -1954,6 +1995,11 @@ export async function handleYeaftChatSend(msg) {
|
|
|
1954
1995
|
appendedUserPrompts,
|
|
1955
1996
|
};
|
|
1956
1997
|
|
|
1998
|
+
// Load the VP persona (defaults to omni) so the engine's worker
|
|
1999
|
+
// prompt has the right identity/role/persona blocks. Without this,
|
|
2000
|
+
// chat mode runs with a generic system prompt instead of Omni.
|
|
2001
|
+
const vpPersona = buildVpPersona(vpId);
|
|
2002
|
+
|
|
1957
2003
|
for await (const event of eng.query({
|
|
1958
2004
|
prompt: text,
|
|
1959
2005
|
promptParts: attachmentBundle.promptParts && attachmentBundle.promptParts.length > 0 ? attachmentBundle.promptParts : null,
|
|
@@ -1961,6 +2007,7 @@ export async function handleYeaftChatSend(msg) {
|
|
|
1961
2007
|
userAlreadyPersisted: false,
|
|
1962
2008
|
threadId: 'main',
|
|
1963
2009
|
senderVpId: vpId,
|
|
2010
|
+
vpPersona,
|
|
1964
2011
|
})) {
|
|
1965
2012
|
resetQueryTimer();
|
|
1966
2013
|
handleEngineEvent(event, handlerCtx);
|
|
@@ -2760,23 +2807,8 @@ export function buildVpQueryOpts({ vpId, groupCoordinator, groupId, envelope, th
|
|
|
2760
2807
|
if (groupMeta && typeof groupMeta.workDir === 'string' && groupMeta.workDir.trim()) {
|
|
2761
2808
|
out.workDir = groupMeta.workDir.trim();
|
|
2762
2809
|
}
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
if (vp) {
|
|
2766
|
-
out.vpPersona = {
|
|
2767
|
-
vpId: resolvedVpId,
|
|
2768
|
-
displayName: vp.displayName || resolvedVpId,
|
|
2769
|
-
displayNameZh: vp.displayNameZh || '',
|
|
2770
|
-
role: vp.role || '',
|
|
2771
|
-
roleZh: vp.roleZh || '',
|
|
2772
|
-
persona: vp.persona || '',
|
|
2773
|
-
// Optional per-VP planning style for the `StartPlan` tool. Empty
|
|
2774
|
-
// string means "fall back to the default template" — the tool
|
|
2775
|
-
// handles the lookup so callers stay ignorant of the default.
|
|
2776
|
-
planInstruction: typeof vp.planInstruction === 'string' ? vp.planInstruction : '',
|
|
2777
|
-
};
|
|
2778
|
-
}
|
|
2779
|
-
} catch { /* persona load is best-effort */ }
|
|
2810
|
+
const persona = buildVpPersona(resolvedVpId);
|
|
2811
|
+
if (persona) out.vpPersona = persona;
|
|
2780
2812
|
if (groupCoordinator && typeof groupCoordinator.ingest === 'function') {
|
|
2781
2813
|
try {
|
|
2782
2814
|
out.router = createRouter({ coordinator: groupCoordinator });
|