@xbrowser/qianwen 1.0.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.
Files changed (2) hide show
  1. package/index.ts +542 -0
  2. package/package.json +37 -0
package/index.ts ADDED
@@ -0,0 +1,542 @@
1
+ import type { XCLIAPI, CommandContext } from '@dyyz1993/xcli-core';
2
+ import { ok, fail } from '@dyyz1993/xcli-core';
3
+ import { z } from 'zod';
4
+ import path from 'path';
5
+ import fs from 'fs';
6
+
7
+ type Page = import('playwright-core').Page;
8
+
9
+ const SITE_URL = 'https://www.qianwen.com';
10
+
11
+ const SEL = {
12
+ input: 'div[role="textbox"][contenteditable="true"]',
13
+ newChat: 'button',
14
+ sendButton: 'button[aria-label="发送消息"]',
15
+ conversationLinks: 'a[href*="/chat/"], [class*="conversation"] a, [class*="session"] a',
16
+ replyContainer: '[class*="markdown"]',
17
+ fileInput: 'input[type="file"]',
18
+ } as const;
19
+
20
+ function getPage(ctx: CommandContext): Page {
21
+ const page = (ctx as unknown as Record<string, unknown>).page as Page | undefined;
22
+ if (!page) throw new Error('需要浏览器页面,请使用 --cdp 参数连接');
23
+ return page;
24
+ }
25
+
26
+ function buildTips(ctx: CommandContext): string[] {
27
+ const tips: string[] = [];
28
+ const ctxAny = ctx as unknown as Record<string, unknown>;
29
+ const options = ctxAny.options as Record<string, unknown> | undefined;
30
+ const cdp = ctxAny.cdpEndpoint || options?.cdp;
31
+ if (!cdp) tips.push('建议使用 --cdp 9221 连接到已登录的浏览器');
32
+ tips.push(`Session: ${ctxAny.sessionId || 'default'}`);
33
+ return tips;
34
+ }
35
+
36
+ async function ensurePage(page: Page, ctx?: CommandContext): Promise<void> {
37
+ const url = page.url();
38
+ if (!url.includes('qianwen.com') && !url.includes('tongyi.aliyun.com')) {
39
+ await page.goto(SITE_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
40
+ await page.waitForTimeout(2000);
41
+ }
42
+ if (ctx) {
43
+ const isLogin = (ctx as unknown as Record<string, unknown>).__loginChecked as boolean;
44
+ if (!isLogin) {
45
+ (ctx as unknown as Record<string, unknown>).__loginChecked = true;
46
+ const hasInput = await page.evaluate(() => {
47
+ const input = document.querySelector('div[role="textbox"][contenteditable="true"]');
48
+ return !!input;
49
+ });
50
+ if (!hasInput) {
51
+ const bodyText = await page.evaluate(() => document.body?.textContent?.trim().slice(0, 300) || '');
52
+ if (bodyText.includes('登录') && !bodyText.includes('通义千问')) {
53
+ const cdp = (ctx as unknown as Record<string, unknown>).cdpEndpoint;
54
+ throw new Error(
55
+ '通义千问 (Qianwen) 未登录!\n' +
56
+ (cdp
57
+ ? ' 使用 --cdp 连接的浏览器未登录通义千问,请先在浏览器中登录。\n 或运行: xbrowser qianwen login'
58
+ : ' 请使用 --cdp 参数连接已登录的浏览器:\n xbrowser qianwen list --cdp http://localhost:9221')
59
+ );
60
+ }
61
+ }
62
+ }
63
+ }
64
+ }
65
+
66
+ async function uploadFileViaDataTransfer(page: Page, absPath: string): Promise<boolean> {
67
+ const data = fs.readFileSync(absPath);
68
+ const b64 = data.toString('base64');
69
+ const mimeMap: Record<string, string> = {
70
+ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
71
+ '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml',
72
+ '.bmp': 'image/bmp', '.ico': 'image/x-icon',
73
+ '.pdf': 'application/pdf', '.txt': 'text/plain', '.md': 'text/markdown',
74
+ '.json': 'application/json', '.csv': 'text/csv', '.html': 'text/html',
75
+ '.ts': 'text/typescript', '.tsx': 'text/typescript', '.js': 'text/javascript',
76
+ '.py': 'text/x-python', '.yaml': 'text/yaml', '.yml': 'text/yaml',
77
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
78
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
79
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
80
+ '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.flac': 'audio/flac',
81
+ '.mp4': 'video/mp4', '.mov': 'video/quicktime', '.avi': 'video/x-msvideo',
82
+ };
83
+ const ext = path.extname(absPath).toLowerCase();
84
+ const mime = mimeMap[ext] || 'application/octet-stream';
85
+
86
+ const result = await page.evaluate(({ b64data, filename, mimeType }) => {
87
+ const fi = document.querySelector('input[type="file"]');
88
+ if (!fi) return false;
89
+
90
+ const byteChars = atob(b64data);
91
+ const byteNums = new Uint8Array(byteChars.length);
92
+ for (let i = 0; i < byteChars.length; i++) {
93
+ byteNums[i] = byteChars.charCodeAt(i);
94
+ }
95
+ const file = new File([byteNums], filename, { type: mimeType });
96
+
97
+ const dt = new DataTransfer();
98
+ dt.items.add(file);
99
+ Object.defineProperty(fi, 'files', { value: dt.files });
100
+ fi.dispatchEvent(new Event('change', { bubbles: true }));
101
+ return fi.files.length > 0;
102
+ }, { b64data: b64, filename: path.basename(absPath), mimeType: mime });
103
+
104
+ return result;
105
+ }
106
+
107
+ export default function (xcli: XCLIAPI): void {
108
+ const site = xcli.createSite({
109
+ name: 'qianwen',
110
+ url: SITE_URL,
111
+ description: '通义千问 (Qianwen) — 会话管理、消息发送、附件上传',
112
+ requiresLogin: true,
113
+ isLogin: async (ctx) => {
114
+ try {
115
+ const page = (ctx as unknown as Record<string, unknown>).page as Page | undefined;
116
+ if (!page) return false;
117
+ const url = page.url();
118
+ if (url.includes('/login') || url.includes('/auth') || url.includes('/passport')) return false;
119
+ const input = await page.evaluate(() => {
120
+ return !!document.querySelector('div[role="textbox"][contenteditable="true"]');
121
+ });
122
+ return input;
123
+ } catch {
124
+ return false;
125
+ }
126
+ },
127
+ });
128
+
129
+ site.command('list', {
130
+ description: '列出所有历史会话',
131
+ scope: 'page',
132
+ parameters: z.object({}),
133
+ result: z.any(),
134
+ examples: [
135
+ { cmd: 'xbrowser qianwen list', description: '列出所有会话' },
136
+ { cmd: 'xbrowser qianwen list --json', description: 'JSON 格式输出' },
137
+ ],
138
+ handler: async (_params, ctx) => {
139
+ try {
140
+ const page = getPage(ctx);
141
+ await ensurePage(page, ctx);
142
+ await page.waitForTimeout(1500);
143
+
144
+ const conversations = await page.evaluate(() => {
145
+ const links = document.querySelectorAll('a[href*="/chat/"], [class*="conversation"] a, [class*="session"] a');
146
+ return Array.from(links).map((a, i) => ({
147
+ index: i,
148
+ title: (a.textContent || '').trim(),
149
+ url: (a as HTMLAnchorElement).href,
150
+ })).filter(c => c.title.length > 0);
151
+ });
152
+
153
+ const tips = buildTips(ctx);
154
+ tips.push(`共 ${conversations.length} 个会话`);
155
+ return {
156
+ data: conversations,
157
+ tips,
158
+ message: `找到 ${conversations.length} 个会话`,
159
+ };
160
+ } catch (error) {
161
+ return fail('未知错误', ['获取会话列表失败']);
162
+ }
163
+ },
164
+ });
165
+
166
+ site.command('new', {
167
+ description: '创建新的空白对话',
168
+ scope: 'browser',
169
+ parameters: z.object({}),
170
+ result: z.any(),
171
+ examples: [
172
+ { cmd: 'xbrowser qianwen new', description: '新建对话' },
173
+ ],
174
+ handler: async (_params, ctx) => {
175
+ try {
176
+ const page = getPage(ctx);
177
+ await ensurePage(page, ctx);
178
+
179
+ const result = await page.evaluate(() => {
180
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
181
+ let node: Text | null;
182
+ while ((node = walker.nextNode() as Text | null)) {
183
+ if (node.textContent?.includes('新建对话')) {
184
+ const parent = node.parentElement;
185
+ if (parent) {
186
+ parent.click();
187
+ return 'clicked';
188
+ }
189
+ }
190
+ }
191
+ return 'not_found';
192
+ });
193
+
194
+ if (result === 'not_found') {
195
+ const fallback = await page.evaluate(() => {
196
+ const plusBtns = document.querySelectorAll('[class*="add"], [class*="new"], [class*="create"], [class*="plus"]');
197
+ if (plusBtns.length > 0) {
198
+ (plusBtns[0] as HTMLElement).click();
199
+ return 'clicked_icon';
200
+ }
201
+ return 'failed';
202
+ });
203
+ if (fallback === 'failed') throw new Error('找不到"新建对话"按钮');
204
+ }
205
+
206
+ await page.waitForTimeout(1500);
207
+ return {
208
+ data: { created: true },
209
+ tips: buildTips(ctx),
210
+ message: '✅ 已创建新对话',
211
+ };
212
+ } catch (error) {
213
+ return fail('未知错误', ['创建新对话失败']);
214
+ }
215
+ },
216
+ });
217
+
218
+ site.command('open', {
219
+ description: '通过标题打开指定会话(模糊匹配)',
220
+ scope: 'browser',
221
+ parameters: z.object({
222
+ title: z.string().describe('会话标题(支持模糊匹配)'),
223
+ }),
224
+ result: z.any(),
225
+ examples: [
226
+ { cmd: 'xbrowser qianwen open "工作计划"', description: '打开指定会话' },
227
+ { cmd: 'xbrowser qianwen open "代码"', description: '模糊匹配打开' },
228
+ ],
229
+ handler: async (params, ctx) => {
230
+ try {
231
+ const page = getPage(ctx);
232
+ await ensurePage(page, ctx);
233
+ await page.waitForTimeout(1000);
234
+
235
+ const clicked = await page.evaluate((title: string) => {
236
+ const links = document.querySelectorAll('a[href*="/chat/"], [class*="conversation"] a, [class*="session"] a');
237
+ for (const link of links) {
238
+ const text = (link.textContent || '').trim();
239
+ if (text.includes(title)) {
240
+ (link as HTMLAnchorElement).click();
241
+ return { found: true, title: text };
242
+ }
243
+ }
244
+ return { found: false, title: '' };
245
+ }, params.title);
246
+
247
+ if (!clicked.found) throw new Error(`未找到包含"${params.title}"的会话`);
248
+
249
+ await page.waitForTimeout(2000);
250
+ return {
251
+ data: { opened: clicked.title },
252
+ tips: buildTips(ctx),
253
+ message: `✅ 已打开会话:${clicked.title}`,
254
+ };
255
+ } catch (error) {
256
+ return fail('未知错误', ['打开会话失败']);
257
+ }
258
+ },
259
+ });
260
+
261
+ site.command('chat', {
262
+ description: '发送消息并等待 AI 回复',
263
+ scope: 'browser',
264
+ parameters: z.object({
265
+ message: z.string().describe('消息内容'),
266
+ attach: z.string().optional().describe('附件路径(图片或文件)'),
267
+ attachType: z.enum(['image', 'file', 'url']).optional().describe('附件类型'),
268
+ think: z.boolean().optional().describe('开启深度思考模式'),
269
+ search: z.boolean().optional().describe('开启联网搜索'),
270
+ showSources: z.boolean().optional().describe('显示联网搜索引用的来源 URL 和域名'),
271
+ }),
272
+ result: z.any(),
273
+ examples: [
274
+ { cmd: 'xbrowser qianwen chat "你好"', description: '发送消息' },
275
+ { cmd: 'xbrowser qianwen chat "分析这张图" --attach /path/to/img.jpg', description: '发送消息+图片' },
276
+ { cmd: 'xbrowser qianwen chat "深度分析" --think', description: '开启深度思考' },
277
+ { cmd: 'xbrowser qianwen chat "最新新闻" --search --showSources', description: '联网搜索+来源' },
278
+ ],
279
+ handler: async (params, ctx) => {
280
+ try {
281
+ const page = getPage(ctx);
282
+ await ensurePage(page, ctx);
283
+ await page.waitForTimeout(3000);
284
+ const tips = buildTips(ctx);
285
+
286
+ if (params.think) {
287
+ const thinkToggled = await page.evaluate(() => {
288
+ const allEls = document.querySelectorAll('*');
289
+ for (const el of allEls) {
290
+ const text = el.textContent?.trim() || '';
291
+ if ((text === '深度思考' || text === '思考' || text.includes('Think'))
292
+ && el.children.length <= 3 && el.offsetParent !== null) {
293
+ const btn = el.closest('button, [role="switch"]') || el.parentElement;
294
+ if (btn instanceof HTMLElement) {
295
+ btn.click();
296
+ return 'toggled';
297
+ }
298
+ }
299
+ }
300
+ return 'not_found';
301
+ });
302
+ if (thinkToggled !== 'not_found') {
303
+ tips.push('已开启深度思考');
304
+ await page.waitForTimeout(500);
305
+ } else {
306
+ tips.push('⚠ 未找到深度思考开关');
307
+ }
308
+ }
309
+
310
+ if (params.search) {
311
+ const searchEnabled = await page.evaluate(() => {
312
+ const allEls = document.querySelectorAll('*');
313
+ for (const el of allEls) {
314
+ const text = el.textContent?.trim() || '';
315
+ if ((text === '联网搜索' || text === '搜索' || text.includes('Search'))
316
+ && el.children.length <= 3 && el.offsetParent !== null) {
317
+ const btn = el.closest('button, [role="switch"]') || el.parentElement;
318
+ if (btn instanceof HTMLElement) {
319
+ btn.click();
320
+ return 'toggled';
321
+ }
322
+ }
323
+ }
324
+ return 'not_found';
325
+ });
326
+ if (searchEnabled !== 'not_found') {
327
+ tips.push('已开启联网搜索');
328
+ await page.waitForTimeout(500);
329
+ } else {
330
+ tips.push('⚠ 未找到联网搜索开关');
331
+ }
332
+ }
333
+
334
+ if (params.attach) {
335
+ const absPath = path.resolve(params.attach);
336
+ if (!fs.existsSync(absPath)) {
337
+ tips.push(`⚠ 附件文件不存在: ${params.attach},跳过附件`);
338
+ } else {
339
+ const uploaded = await uploadFileViaDataTransfer(page, absPath);
340
+ if (uploaded) {
341
+ tips.push(`已上传附件: ${path.basename(absPath)}`);
342
+ await page.waitForTimeout(1500);
343
+ } else {
344
+ tips.push('⚠ 上传失败,未找到文件输入控件');
345
+ }
346
+ }
347
+ }
348
+
349
+ const inputLocator = page.locator('div[role="textbox"][contenteditable="true"]').first();
350
+ if (await inputLocator.count() === 0) throw new Error('找不到消息输入框');
351
+ await inputLocator.click();
352
+ await page.waitForTimeout(200);
353
+ await page.evaluate(() => {
354
+ const el = document.querySelector('div[role="textbox"][contenteditable="true"]');
355
+ if (el) { el.textContent = ''; }
356
+ });
357
+ await page.keyboard.type(params.message, { delay: 30 });
358
+
359
+ await page.waitForTimeout(500);
360
+
361
+ const sendClicked = await page.evaluate(() => {
362
+ const btn = document.querySelector('button[aria-label="发送消息"]') as HTMLButtonElement | null;
363
+ if (btn) {
364
+ btn.click();
365
+ return true;
366
+ }
367
+ return false;
368
+ });
369
+
370
+ if (!sendClicked) {
371
+ await page.keyboard.press('Enter');
372
+ }
373
+
374
+ tips.push('消息已发送,等待 AI 回复...');
375
+ await page.waitForTimeout(2000);
376
+
377
+ let responseText = '';
378
+ const startTime = Date.now();
379
+ while (Date.now() - startTime < 60000) {
380
+ await page.waitForTimeout(1500);
381
+ try {
382
+ responseText = await page.evaluate((msg: string) => {
383
+ const pageText = document.body.textContent || '';
384
+
385
+ if (pageText.includes('生成中') || pageText.includes('思考中') || pageText.includes('停止生成')) return '';
386
+
387
+ const answerCards = document.querySelectorAll('div.answer-common-card');
388
+ for (let i = answerCards.length - 1; i >= 0; i--) {
389
+ const txt = (answerCards[i].textContent || '').trim();
390
+ if (txt.length > 10) {
391
+ const lines = txt.split('\n').filter(l => !l.includes('思考已完成') && !l.includes('Qwen3-Max-Thinking') && l.trim().length > 0);
392
+ if (lines.length > 0) return lines.join('\n').slice(0, 2000);
393
+ }
394
+ }
395
+
396
+ const chatAnswers = document.querySelectorAll('div.chat-answers-card-wrap');
397
+ for (let i = chatAnswers.length - 1; i >= 0; i--) {
398
+ const txt = (chatAnswers[i].textContent || '').trim();
399
+ if (txt.length > msg.length + 20) {
400
+ const lines = txt.split('\n').filter(l => !l.includes('Qwen3') && !l.includes('思考已完成') && !l.includes('用户发送了一条') && l.trim().length > 5);
401
+ if (lines.length > 0) return lines.join('\n').slice(0, 2000);
402
+ }
403
+ }
404
+
405
+ const markdownEls = document.querySelectorAll('[class*="markdown"]');
406
+ for (let i = markdownEls.length - 1; i >= 0; i--) {
407
+ const txt = (markdownEls[i].textContent || '').trim();
408
+ const parentText = markdownEls[i].parentElement?.textContent || '';
409
+ if (txt.length > 10 && !txt.includes(msg) && !parentText.includes('Qwen3-Max-Thinking')) {
410
+ const lines = txt.split('\n').filter(l => !l.includes('思考已完成') && l.trim().length > 0);
411
+ if (lines.length > 0) return lines.join('\n').slice(0, 2000);
412
+ }
413
+ }
414
+
415
+ return '';
416
+ }, params.message);
417
+ if (responseText) break;
418
+ } catch {
419
+ // ignore
420
+ }
421
+ }
422
+
423
+ if (responseText) {
424
+ tips.push('AI 回复已收到');
425
+ return {
426
+ data: {
427
+ response: responseText,
428
+ duration: `${((Date.now() - startTime) / 1000).toFixed(1)}s`,
429
+ },
430
+ tips,
431
+ message: `✅ AI 回复 (${((Date.now() - startTime) / 1000).toFixed(1)}s)`,
432
+ };
433
+ } else {
434
+ tips.push('AI 回复超时或未检测到');
435
+ return {
436
+ data: { response: '' },
437
+ tips,
438
+ message: '⏱ AI 回复超时(60s),请检查页面',
439
+ };
440
+ }
441
+ } catch (error) {
442
+ return fail('未知错误', ['发送消息失败']);
443
+ }
444
+ },
445
+ });
446
+
447
+ site.command('attach', {
448
+ description: '上传附件(图片或文件)',
449
+ scope: 'browser',
450
+ parameters: z.object({
451
+ file: z.string().describe('文件路径'),
452
+ }),
453
+ result: z.any(),
454
+ examples: [
455
+ { cmd: 'xbrowser qianwen attach /path/to/img.jpg', description: '上传图片' },
456
+ { cmd: 'xbrowser qianwen attach /path/to/doc.pdf', description: '上传文件' },
457
+ ],
458
+ handler: async (params, ctx) => {
459
+ try {
460
+ const page = getPage(ctx);
461
+ await ensurePage(page, ctx);
462
+ await page.waitForTimeout(500);
463
+ const tips = buildTips(ctx);
464
+
465
+ const absPath = path.resolve(params.file);
466
+ if (!fs.existsSync(absPath)) {
467
+ throw new Error(`文件不存在: ${absPath}`);
468
+ }
469
+
470
+ const uploaded = await uploadFileViaDataTransfer(page, absPath);
471
+ if (!uploaded) {
472
+ throw new Error('找不到 file input,上传失败');
473
+ }
474
+
475
+ await page.waitForTimeout(1000);
476
+ tips.push(`附件 "${path.basename(absPath)}" 已上传`);
477
+ return {
478
+ data: { file: absPath, uploaded: true },
479
+ tips,
480
+ message: `✅ 附件 "${path.basename(absPath)}" 已上传`,
481
+ };
482
+ } catch (error) {
483
+ return fail('未知错误', ['上传附件失败']);
484
+ }
485
+ },
486
+ });
487
+
488
+ site.login(async (ctx) => {
489
+ const page = (ctx as unknown as Record<string, unknown>).page as Page | undefined;
490
+ const cdp = (ctx as unknown as Record<string, unknown>).cdpEndpoint;
491
+ const sessionId = (ctx as unknown as Record<string, unknown>).sessionId as string | undefined;
492
+
493
+ if (cdp && page) {
494
+ await page.goto(SITE_URL, { waitUntil: 'domcontentloaded', timeout: 30000 }).catch(() => {});
495
+ await page.waitForTimeout(2000);
496
+ const loggedIn = await site.isLoggedIn(ctx).catch(() => false);
497
+ if (loggedIn) {
498
+ console.log('✅ CDP 浏览器已登录通义千问');
499
+ return;
500
+ }
501
+ }
502
+
503
+ if (sessionId || cdp) {
504
+ console.log('');
505
+ console.log('🔑 请使用 Viewer 在浏览器中登录通义千问:');
506
+ console.log(' 1. 打开 Viewer(实时查看浏览器画面):');
507
+ console.log(` agent-browser viewer --session ${sessionId || 'default'}`);
508
+ console.log(' 2. 在 Viewer 页面中登录通义千问');
509
+ console.log(' 3. 登录后回到此终端,按 Enter 继续');
510
+ console.log('');
511
+ console.log(' 也可以用截图模式查看当前页面状态:');
512
+ console.log(' xbrowser screenshot --session ' + (sessionId || 'default'));
513
+ console.log('');
514
+ } else if (!cdp) {
515
+ console.log('');
516
+ console.log('⚠️ 推荐使用 --cdp 参数连接到已登录的浏览器:');
517
+ console.log(' xbrowser qianwen list --cdp http://localhost:9221');
518
+ console.log('');
519
+ console.log('🔑 或者启动 Viewer 手动登录:');
520
+ console.log(' 1. 启动浏览器会话:');
521
+ console.log(' xbrowser session open ' + SITE_URL + ' --name qw-login');
522
+ console.log(' 2. 启动 Viewer:');
523
+ console.log(' agent-browser viewer --session qw-login');
524
+ console.log(' 3. 在 Viewer 中登录后:');
525
+ console.log(' xbrowser qianwen list --session qw-login');
526
+ console.log('');
527
+ }
528
+
529
+ if (page) {
530
+ await page.goto(SITE_URL, { waitUntil: 'domcontentloaded', timeout: 30000 }).catch(() => {});
531
+ }
532
+ console.log(' 按 Enter 继续...');
533
+ await new Promise<void>((resolve) => {
534
+ process.stdin.once('data', () => resolve());
535
+ });
536
+ console.log('✅ 继续执行');
537
+ });
538
+
539
+ site.logout(async (_ctx) => {
540
+ console.log('⚠️ 请在浏览器中手动退出通义千问登录');
541
+ });
542
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@xbrowser/qianwen",
3
+ "version": "1.0.0",
4
+ "description": "通义千问 (Qianwen) - 会话管理、消息发送、文件附件",
5
+ "type": "module",
6
+ "main": "index.ts",
7
+ "keywords": [
8
+ "xbrowser",
9
+ "qianwen",
10
+ "tongyi",
11
+ "aliyun",
12
+ "ai",
13
+ "cdp"
14
+ ],
15
+ "author": "XBrowser Team",
16
+ "license": "MIT",
17
+ "dependencies": {
18
+ "zod": "^3.24.0"
19
+ },
20
+ "peerDependencies": {
21
+ "@dyyz1993/xcli-core": ">=1.0.0"
22
+ },
23
+ "xbrowser": {
24
+ "name": "qianwen",
25
+ "description": "通义千问 (Qianwen) AI 助手",
26
+ "commands": [
27
+ "list",
28
+ "new",
29
+ "open",
30
+ "chat",
31
+ "attach"
32
+ ],
33
+ "sites": [
34
+ "qianwen"
35
+ ]
36
+ }
37
+ }