xiaozuoassistant 0.2.48 → 0.2.50

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.
@@ -509,8 +509,44 @@ app.get('/api/plugins', (req, res) => {
509
509
  });
510
510
  // 存储 OAuth 状态以防止 CSRF 攻击
511
511
  const oauthStates = new Map();
512
- // 模拟的 Token 存储 (实际应存入 SQLite 或加密文件)
512
+ // 模拟的 Token 存储 (持久化到文件)
513
+ const tokensFilePath = path.join(process.cwd(), 'data', 'oauth-tokens.json');
513
514
  const oauthTokens = new Map();
515
+ // 初始化时加载 Token
516
+ try {
517
+ if (fs.existsSync(tokensFilePath)) {
518
+ const data = JSON.parse(fs.readFileSync(tokensFilePath, 'utf8'));
519
+ for (const key of Object.keys(data)) {
520
+ oauthTokens.set(key, data[key]);
521
+ }
522
+ }
523
+ }
524
+ catch (e) {
525
+ console.warn('[OAuth] Failed to load tokens:', e);
526
+ }
527
+ function saveTokens() {
528
+ try {
529
+ const dir = path.dirname(tokensFilePath);
530
+ if (!fs.existsSync(dir))
531
+ fs.mkdirSync(dir, { recursive: true });
532
+ const data = Object.fromEntries(oauthTokens);
533
+ fs.writeFileSync(tokensFilePath, JSON.stringify(data, null, 2));
534
+ }
535
+ catch (e) {
536
+ console.error('[OAuth] Failed to save tokens:', e);
537
+ }
538
+ }
539
+ // 暴露一个简单的内部 API 供插件获取 Token
540
+ app.get('/internal/api/auth/tokens/:provider', (req, res) => {
541
+ // 这个接口仅供本地/内部调用
542
+ const token = oauthTokens.get(req.params.provider);
543
+ if (token) {
544
+ res.json(token);
545
+ }
546
+ else {
547
+ res.status(404).json({ error: 'Token not found' });
548
+ }
549
+ });
514
550
  // Mock OAuth 授权接口
515
551
  app.get('/api/auth/:provider/authorize', (req, res) => {
516
552
  const provider = req.params.provider;
@@ -583,7 +619,8 @@ app.get('/api/auth/:provider/callback', async (req, res) => {
583
619
  headers: { 'Authorization': `Bearer ${appAccessToken}` }
584
620
  });
585
621
  tokenData = userTokenRes.data.data;
586
- oauthTokens.set('lark', tokenData); // 实际应持久化
622
+ oauthTokens.set('lark', tokenData);
623
+ saveTokens();
587
624
  }
588
625
  else if (provider === 'wps') {
589
626
  const appId = process.env.WPS_APP_ID || '';
@@ -599,7 +636,8 @@ app.get('/api/auth/:provider/callback', async (req, res) => {
599
636
  code: code
600
637
  });
601
638
  tokenData = tokenRes.data;
602
- oauthTokens.set('wps', tokenData); // 实际应持久化
639
+ oauthTokens.set('wps', tokenData);
640
+ saveTokens();
603
641
  }
604
642
  console.log(`[OAuth] Successfully authenticated with ${provider}`);
605
643
  // 更新内存中的插件状态为已启用
@@ -1,5 +1,59 @@
1
1
  import { BaseSkill } from '../../../skills/base-skill.js';
2
2
  import * as lark from '@larksuiteoapi/node-sdk';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ // 获取可能通过网页授权获取到的 user_access_token
6
+ function getUserAccessToken() {
7
+ try {
8
+ const tokensFilePath = path.join(process.cwd(), 'data', 'oauth-tokens.json');
9
+ if (fs.existsSync(tokensFilePath)) {
10
+ const data = JSON.parse(fs.readFileSync(tokensFilePath, 'utf8'));
11
+ if (data.lark && data.lark.access_token) {
12
+ return data.lark.access_token;
13
+ }
14
+ }
15
+ }
16
+ catch (e) {
17
+ // Ignore errors
18
+ }
19
+ return undefined;
20
+ }
21
+ // Helper function to resolve document token, especially handling /wiki/ URLs
22
+ async function resolveDocumentToken(client, urlOrToken) {
23
+ let token = urlOrToken;
24
+ let isWiki = false;
25
+ if (urlOrToken.includes('/docx/')) {
26
+ token = urlOrToken.split('/docx/')[1].split(/[?#]/)[0];
27
+ return { token, type: 'docx' };
28
+ }
29
+ else if (urlOrToken.includes('/doc/')) {
30
+ token = urlOrToken.split('/doc/')[1].split(/[?#]/)[0];
31
+ return { token, type: 'doc' };
32
+ }
33
+ else if (urlOrToken.includes('/wiki/')) {
34
+ token = urlOrToken.split('/wiki/')[1].split(/[?#]/)[0];
35
+ isWiki = true;
36
+ }
37
+ else if (urlOrToken.startsWith('wikcn')) {
38
+ isWiki = true;
39
+ }
40
+ const userToken = getUserAccessToken();
41
+ const options = userToken ? lark.withUserAccessToken(userToken) : undefined;
42
+ if (isWiki) {
43
+ const res = await client.wiki.space.getNode({
44
+ params: { token }
45
+ }, options);
46
+ if (res.code !== 0) {
47
+ throw new Error(`Failed to resolve wiki token. Code: ${res.code}, Msg: ${res.msg}`);
48
+ }
49
+ return {
50
+ token: res.data?.node?.obj_token || token,
51
+ type: res.data?.node?.obj_type || 'docx'
52
+ };
53
+ }
54
+ // Fallback assuming it's a docx token if not wiki
55
+ return { token, type: 'docx' };
56
+ }
3
57
  export class ReadLarkDocSkill extends BaseSkill {
4
58
  constructor() {
5
59
  super(...arguments);
@@ -26,18 +80,6 @@ export class ReadLarkDocSkill extends BaseSkill {
26
80
  }
27
81
  async execute(args, ctx) {
28
82
  try {
29
- // Extract document token from URL
30
- // Typical Lark doc URL formats:
31
- // https://domain.feishu.cn/docx/TOKEN
32
- // https://domain.feishu.cn/wiki/WIKITOKEN
33
- let documentToken = args.document_url;
34
- // Basic extraction logic
35
- if (args.document_url.includes('/docx/')) {
36
- documentToken = args.document_url.split('/docx/')[1].split(/[?#]/)[0];
37
- }
38
- else if (args.document_url.includes('/doc/')) {
39
- documentToken = args.document_url.split('/doc/')[1].split(/[?#]/)[0];
40
- }
41
83
  // Fetch config to get app_id and app_secret if not provided
42
84
  const configLoader = require('../../../config/loader.js').config;
43
85
  let appId = args.app_id;
@@ -67,13 +109,20 @@ export class ReadLarkDocSkill extends BaseSkill {
67
109
  appId: appId,
68
110
  appSecret: appSecret,
69
111
  });
112
+ // Extract and resolve document token from URL
113
+ const { token: documentToken, type: objType } = await resolveDocumentToken(client, args.document_url);
114
+ if (objType !== 'docx' && objType !== 'doc') {
115
+ return { error: `Unsupported document type: ${objType}. Only docx and doc are supported for reading raw text content.` };
116
+ }
70
117
  // We use the drive/v1/export API or docx/v1/documents API to read content
71
118
  // Let's try the newer DOCX API first to get raw content
119
+ const userToken = getUserAccessToken();
120
+ const options = userToken ? lark.withUserAccessToken(userToken) : undefined;
72
121
  const res = await client.docx.document.rawContent({
73
122
  path: {
74
123
  document_id: documentToken,
75
124
  },
76
- });
125
+ }, options);
77
126
  if (res.code !== 0) {
78
127
  return { error: `Failed to read Lark document. Code: ${res.code}, Msg: ${res.msg}. Make sure the App has the "docs:doc:readonly" permission and has been added as a collaborator to the document.` };
79
128
  }
@@ -140,12 +189,14 @@ export class CreateLarkDocSkill extends BaseSkill {
140
189
  }
141
190
  }
142
191
  const client = new lark.Client({ appId: appId, appSecret: appSecret });
192
+ const userToken = getUserAccessToken();
193
+ const options = userToken ? lark.withUserAccessToken(userToken) : undefined;
143
194
  const res = await client.docx.document.create({
144
195
  data: {
145
196
  title: args.title,
146
197
  folder_token: args.folder_token || ''
147
198
  }
148
- });
199
+ }, options);
149
200
  if (res.code !== 0) {
150
201
  return { error: `Failed to create document. Code: ${res.code}, Msg: ${res.msg}` };
151
202
  }
@@ -185,13 +236,6 @@ export class AppendLarkDocSkill extends BaseSkill {
185
236
  }
186
237
  async execute(args, ctx) {
187
238
  try {
188
- let documentToken = args.document_url;
189
- if (args.document_url.includes('/docx/')) {
190
- documentToken = args.document_url.split('/docx/')[1].split(/[?#]/)[0];
191
- }
192
- else if (args.document_url.includes('/doc/')) {
193
- documentToken = args.document_url.split('/doc/')[1].split(/[?#]/)[0];
194
- }
195
239
  const configLoader = require('../../../config/loader.js').config;
196
240
  let appId = args.app_id;
197
241
  let appSecret = args.app_secret;
@@ -215,6 +259,10 @@ export class AppendLarkDocSkill extends BaseSkill {
215
259
  }
216
260
  }
217
261
  const client = new lark.Client({ appId: appId, appSecret: appSecret });
262
+ const { token: documentToken, type: objType } = await resolveDocumentToken(client, args.document_url);
263
+ if (objType !== 'docx') {
264
+ return { error: `Unsupported document type: ${objType}. Append operation currently only supports docx documents.` };
265
+ }
218
266
  // Create blocks from content
219
267
  const lines = args.content.split('\n').filter(l => l.trim() !== '');
220
268
  const children = lines.map(line => ({
@@ -229,6 +277,8 @@ export class AppendLarkDocSkill extends BaseSkill {
229
277
  }));
230
278
  if (children.length === 0)
231
279
  return { success: true, message: 'Nothing to append' };
280
+ const userToken = getUserAccessToken();
281
+ const options = userToken ? lark.withUserAccessToken(userToken) : undefined;
232
282
  const res = await client.docx.documentBlockChildren.create({
233
283
  path: {
234
284
  document_id: documentToken,
@@ -238,7 +288,7 @@ export class AppendLarkDocSkill extends BaseSkill {
238
288
  children: children,
239
289
  index: -1
240
290
  }
241
- });
291
+ }, options);
242
292
  if (res.code !== 0) {
243
293
  return { error: `Failed to append to document. Code: ${res.code}, Msg: ${res.msg}` };
244
294
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xiaozuoassistant",
3
- "version": "0.2.48",
3
+ "version": "0.2.50",
4
4
  "description": "A local-first personal AI assistant with multi-channel support and enhanced memory.",
5
5
  "author": "mantle.lau",
6
6
  "license": "MIT",