yuangs 1.3.38 → 1.3.41

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 (57) hide show
  1. package/README.md +44 -0
  2. package/dist/ai/client.d.ts +9 -0
  3. package/dist/ai/client.js +118 -0
  4. package/dist/ai/client.js.map +1 -0
  5. package/dist/ai/prompt.d.ts +3 -0
  6. package/dist/ai/prompt.js +56 -0
  7. package/dist/ai/prompt.js.map +1 -0
  8. package/dist/ai/types.d.ts +5 -0
  9. package/dist/ai/types.js +3 -0
  10. package/dist/ai/types.js.map +1 -0
  11. package/dist/cli.d.ts +2 -0
  12. package/dist/cli.js +125 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/commands/handleAIChat.d.ts +1 -0
  15. package/dist/commands/handleAIChat.js +92 -0
  16. package/dist/commands/handleAIChat.js.map +1 -0
  17. package/dist/commands/handleAICommand.d.ts +4 -0
  18. package/dist/commands/handleAICommand.js +97 -0
  19. package/dist/commands/handleAICommand.js.map +1 -0
  20. package/dist/commands/handleConfig.d.ts +1 -0
  21. package/dist/commands/handleConfig.js +73 -0
  22. package/dist/commands/handleConfig.js.map +1 -0
  23. package/dist/core/apps.d.ts +7 -0
  24. package/dist/core/apps.js +64 -0
  25. package/dist/core/apps.js.map +1 -0
  26. package/dist/core/autofix.d.ts +3 -0
  27. package/dist/core/autofix.js +24 -0
  28. package/dist/core/autofix.js.map +1 -0
  29. package/dist/core/executor.d.ts +6 -0
  30. package/dist/core/executor.js +28 -0
  31. package/dist/core/executor.js.map +1 -0
  32. package/dist/core/macros.d.ts +9 -0
  33. package/dist/core/macros.js +51 -0
  34. package/dist/core/macros.js.map +1 -0
  35. package/dist/core/os.d.ts +7 -0
  36. package/dist/core/os.js +36 -0
  37. package/dist/core/os.js.map +1 -0
  38. package/dist/core/risk.d.ts +1 -0
  39. package/dist/core/risk.js +11 -0
  40. package/dist/core/risk.js.map +1 -0
  41. package/dist/index.d.ts +1 -0
  42. package/dist/index.js +3 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/utils/confirm.d.ts +1 -0
  45. package/dist/utils/confirm.js +21 -0
  46. package/dist/utils/confirm.js.map +1 -0
  47. package/dist/utils/history.d.ts +10 -0
  48. package/dist/utils/history.js +31 -0
  49. package/dist/utils/history.js.map +1 -0
  50. package/package.json +25 -9
  51. package/cli.js +0 -560
  52. package/index.js +0 -361
  53. package/test/index.test.js +0 -78
  54. package/test/macros.test.js +0 -91
  55. package/yuangs.config.example.json +0 -11
  56. package/yuangs.config.example.yaml +0 -23
  57. package/yuangs.config.json +0 -9
package/index.js DELETED
@@ -1,361 +0,0 @@
1
- const { exec } = require('child_process');
2
- const axios = require('axios');
3
- const fs = require('fs');
4
- const path = require('path');
5
- const os = require('os');
6
-
7
- // Store conversation history
8
- // 存储结构标准为: [{ role: 'user', content: '...' }, { role: 'assistant', content: '...' }]
9
- let conversationHistory = [];
10
-
11
- const HISTORY_FILE = path.join(os.homedir(), '.yuangs_cmd_history.json');
12
- const CONFIG_FILE = path.join(os.homedir(), '.yuangs.json');
13
- const MACROS_FILE = path.join(os.homedir(), '.yuangs_macros.json');
14
-
15
- function getUserConfig() {
16
- if (fs.existsSync(CONFIG_FILE)) {
17
- try {
18
- return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
19
- } catch (e) {}
20
- }
21
- return {};
22
- }
23
-
24
- function getCommandHistory() {
25
- if (fs.existsSync(HISTORY_FILE)) {
26
- try {
27
- return JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf8'));
28
- } catch (e) {}
29
- }
30
- return [];
31
- }
32
-
33
- function saveSuccessfulCommand(question, command) {
34
- if (!command) return;
35
- let history = getCommandHistory();
36
- const newEntry = { question, command, time: new Date().toLocaleString() };
37
- history = [newEntry, ...history.filter(item => item.command !== command)].slice(0, 5);
38
- fs.writeFileSync(HISTORY_FILE, JSON.stringify(history, null, 2));
39
- }
40
-
41
- function getMacros() {
42
- if (fs.existsSync(MACROS_FILE)) {
43
- try {
44
- return JSON.parse(fs.readFileSync(MACROS_FILE, 'utf8'));
45
- } catch (e) {}
46
- }
47
- return {};
48
- }
49
-
50
- function saveMacro(name, commands, description = '') {
51
- const macros = getMacros();
52
- macros[name] = {
53
- commands,
54
- description,
55
- createdAt: new Date().toISOString()
56
- };
57
- fs.writeFileSync(MACROS_FILE, JSON.stringify(macros, null, 2));
58
- return true;
59
- }
60
-
61
- function deleteMacro(name) {
62
- const macros = getMacros();
63
- if (macros[name]) {
64
- delete macros[name];
65
- fs.writeFileSync(MACROS_FILE, JSON.stringify(macros, null, 2));
66
- return true;
67
- }
68
- return false;
69
- }
70
-
71
- // Default apps (fallback if no config file exists)
72
- const DEFAULT_APPS = {
73
- shici: 'https://wealth.want.biz/shici/index.html',
74
- dict: 'https://wealth.want.biz/pages/dict.html',
75
- pong: 'https://wealth.want.biz/pages/pong.html'
76
- };
77
-
78
- // Load apps from configuration file
79
- function loadAppsConfig() {
80
- // Define possible config file locations (JSON and YAML)
81
- const configPaths = [
82
- path.join(process.cwd(), 'yuangs.config.json'), // Current working directory
83
- path.join(process.cwd(), '.yuangs.json'), // Current working directory dot file
84
- path.join(process.cwd(), 'yuangs.config.yaml'), // Current working directory YAML
85
- path.join(process.cwd(), 'yuangs.config.yml'), // Current working directory YAML
86
- path.join(process.cwd(), '.yuangs.yaml'), // Current working directory dot YAML
87
- path.join(process.cwd(), '.yuangs.yml'), // Current working directory dot YAML
88
- path.join(require('os').homedir(), '.yuangs.json'), // User home directory
89
- path.join(require('os').homedir(), '.yuangs.yaml'), // User home directory YAML
90
- path.join(require('os').homedir(), '.yuangs.yml'), // User home directory YAML
91
- path.join(__dirname, 'yuangs.config.json'), // Project directory
92
- path.join(__dirname, '.yuangs.json'), // Project directory dot file
93
- path.join(__dirname, 'yuangs.config.yaml'), // Project directory YAML
94
- path.join(__dirname, 'yuangs.config.yml') // Project directory YAML
95
- ];
96
-
97
- for (const configPath of configPaths) {
98
- if (fs.existsSync(configPath)) {
99
- try {
100
- const configContent = fs.readFileSync(configPath, 'utf8');
101
-
102
- // Determine if it's JSON or YAML based on file extension
103
- let config;
104
- if (configPath.endsWith('.json')) {
105
- config = JSON.parse(configContent);
106
- } else {
107
- // For YAML files, we need to require the yaml parser
108
- let yaml;
109
- try {
110
- yaml = require('js-yaml');
111
- } catch (yamlError) {
112
- console.warn(`Warning: js-yaml not installed, skipping YAML file ${configPath}`);
113
- console.warn('Install js-yaml with: npm install js-yaml');
114
- continue; // Skip this file and try the next config file
115
- }
116
- config = yaml.load(configContent);
117
- }
118
-
119
- // If config has an 'apps' property, use it, otherwise use the whole config as apps
120
- return config.apps || config;
121
- } catch (error) {
122
- console.warn(`Warning: Could not parse config file at ${configPath}:`, error.message);
123
- // Continue to next config file
124
- }
125
- }
126
- }
127
-
128
- // If no config file is found, use default apps
129
- return DEFAULT_APPS;
130
- }
131
-
132
- const APPS = loadAppsConfig();
133
-
134
- function openUrl(url) {
135
- let command;
136
- switch (process.platform) {
137
- case 'darwin': command = `open "${url}"`; break;
138
- case 'win32': command = `start "${url}"`; break;
139
- default: command = `xdg-open "${url}"`; break;
140
- }
141
- exec(command);
142
- }
143
-
144
- // Function to add a message to the conversation history
145
- function addToConversationHistory(role, content) {
146
- conversationHistory.push({ role, content });
147
-
148
- // Keep only the last 20 messages to prevent history from growing too large
149
- if (conversationHistory.length > 20) {
150
- conversationHistory = conversationHistory.slice(-20);
151
- }
152
- }
153
-
154
- // Function to clear conversation history
155
- function clearConversationHistory() {
156
- conversationHistory = [];
157
- }
158
-
159
- // Function to get conversation history
160
- function getConversationHistory() {
161
- return conversationHistory;
162
- }
163
-
164
- /**
165
- * 通用 AI 调用函数 (OpenAI 兼容接口)
166
- */
167
- async function callAI_Stream(messages, model, onChunk) {
168
- const config = getUserConfig();
169
- const url = config.aiProxyUrl || 'https://aiproxy.want.biz/v1/chat/completions';
170
-
171
- const response = await axios({
172
- method: 'post',
173
- url: url,
174
- data: {
175
- model: model || config.defaultModel || 'Assistant',
176
- messages: messages,
177
- stream: true
178
- },
179
- responseType: 'stream',
180
- headers: {
181
- 'Content-Type': 'application/json',
182
- 'X-Client-ID': 'npm_yuangs',
183
- 'Origin': 'https://cli.want.biz',
184
- 'Referer': 'https://cli.want.biz/',
185
- 'account': config.accountType || 'free',
186
- 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1',
187
- 'Accept': 'application/json'
188
- }
189
- });
190
-
191
- return new Promise((resolve, reject) => {
192
- let buffer = '';
193
- response.data.on('data', chunk => {
194
- buffer += chunk.toString();
195
- let lines = buffer.split('\n');
196
- // 数组的最后一个元素可能是不完整的行,留到下一次处理
197
- buffer = lines.pop();
198
-
199
- for (const line of lines) {
200
- const trimmedLine = line.trim();
201
- if (trimmedLine.startsWith('data: ')) {
202
- const data = trimmedLine.slice(6);
203
- if (data === '[DONE]') {
204
- resolve();
205
- return;
206
- }
207
- try {
208
- const parsed = JSON.parse(data);
209
- const content = parsed.choices[0]?.delta?.content || '';
210
- if (content) onChunk(content);
211
- } catch (e) {
212
- // 如果这一行真的有问题,忽略它
213
- }
214
- }
215
- }
216
- });
217
- response.data.on('error', reject);
218
- response.data.on('end', () => {
219
- // 处理缓冲区中剩余的内容(如果有)
220
- if (buffer.trim().startsWith('data: ')) {
221
- try {
222
- const data = buffer.trim().slice(6);
223
- if (data !== '[DONE]') {
224
- const parsed = JSON.parse(data);
225
- const content = parsed.choices[0]?.delta?.content || '';
226
- if (content) onChunk(content);
227
- }
228
- } catch (e) {}
229
- }
230
- resolve();
231
- });
232
- });
233
- }
234
-
235
- async function callAI_OpenAI(messages, model) {
236
- const config = getUserConfig();
237
- const url = config.aiProxyUrl || 'https://aiproxy.want.biz/v1/chat/completions';
238
-
239
- const headers = {
240
- 'Content-Type': 'application/json',
241
- 'X-Client-ID': 'npm_yuangs',
242
- 'Origin': 'https://cli.want.biz',
243
- 'Referer': 'https://cli.want.biz/',
244
- 'account': config.accountType || 'free',
245
- 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1',
246
- 'Accept': 'application/json'
247
- };
248
-
249
- const data = {
250
- model: model || config.defaultModel || 'Assistant',
251
- messages: messages,
252
- stream: false
253
- };
254
-
255
- return await axios.post(url, data, { headers });
256
- }
257
-
258
- /**
259
- * 获取 AI 回复
260
- */
261
- async function getAIAnswer(question, model, includeHistory = true) {
262
- // 构建 messages 数组 (上下文 + 当前问题)
263
- let messages = [];
264
- if (includeHistory) {
265
- messages = [...conversationHistory];
266
- }
267
- messages.push({ role: 'user', content: question });
268
-
269
- try {
270
- const response = await callAI_OpenAI(messages, model);
271
- const aiContent = response.data?.choices?.[0]?.message?.content;
272
-
273
- if (!aiContent) {
274
- throw new Error('Invalid response structure from AI API');
275
- }
276
-
277
- // 只有请求成功才记录历史
278
- addToConversationHistory('user', question);
279
- addToConversationHistory('assistant', aiContent);
280
-
281
- return {
282
- explanation: aiContent, // 兼容字段
283
- content: aiContent, // 标准字段建议
284
- raw: response.data // 原始响应
285
- };
286
-
287
- } catch (error) {
288
- const errorMsg = error.response?.data?.error?.message || error.response?.data?.message || error.message || '未知错误';
289
- console.error('AI 请求失败:', errorMsg);
290
- return null;
291
- }
292
- }
293
-
294
- async function generateCommand(instruction, model) {
295
- const config = getUserConfig();
296
- const messages = [
297
- {
298
- role: 'system',
299
- content: `You are a Linux command generator. Convert the user's natural language request into a single, executable Linux command.
300
- IMPORTANT: Output ONLY the command. Do not check for safety. Do not output markdown code blocks (no backticks). Do not explain.`
301
- },
302
- {
303
- role: 'user',
304
- content: `Request: ${instruction}`
305
- }
306
- ];
307
-
308
- try {
309
- const response = await callAI_OpenAI(messages, model || config.defaultModel);
310
- const aiContent = response.data?.choices?.[0]?.message?.content;
311
-
312
- if (aiContent) {
313
- let command = aiContent.trim();
314
- if (command.startsWith('`') && command.endsWith('`')) {
315
- command = command.slice(1, -1);
316
- }
317
- if (command.startsWith('```') && command.endsWith('```')) {
318
- command = command.split('\n').filter(line => !line.startsWith('```')).join('\n').trim();
319
- }
320
- if (command.startsWith('$ ')) command = command.slice(2);
321
- if (command.startsWith('> ')) command = command.slice(2);
322
-
323
- return command;
324
- }
325
- return null;
326
- } catch (error) {
327
- return null;
328
- }
329
- }
330
-
331
- module.exports = {
332
- urls: APPS,
333
- openApp: (appKey) => {
334
- const url = APPS[appKey];
335
- if (url) {
336
- openUrl(url);
337
- return true;
338
- }
339
- console.error(`App '${appKey}' not found`);
340
- return false;
341
- },
342
- openShici: () => openUrl(APPS.shici || DEFAULT_APPS.shici),
343
- openDict: () => openUrl(APPS.dict || DEFAULT_APPS.dict),
344
- openPong: () => openUrl(APPS.pong || DEFAULT_APPS.pong),
345
- listApps: () => {
346
- console.log('--- YGS Apps ---');
347
- Object.entries(APPS).forEach(([key, url]) => console.log(`${key}: ${url}`));
348
- },
349
- getAIAnswer,
350
- addToConversationHistory,
351
- clearConversationHistory,
352
- getConversationHistory,
353
- generateCommand,
354
- getUserConfig,
355
- getCommandHistory,
356
- saveSuccessfulCommand,
357
- callAI_Stream,
358
- getMacros,
359
- saveMacro,
360
- deleteMacro
361
- };
@@ -1,78 +0,0 @@
1
- const { exec } = require('child_process');
2
- const path = require('path');
3
- const yuangs = require('../index.js');
4
-
5
- describe('Module: index.js', () => {
6
- beforeEach(() => {
7
- yuangs.clearConversationHistory();
8
- });
9
-
10
- test('should export correct app URLs', () => {
11
- expect(yuangs.urls).toHaveProperty('shici');
12
- expect(yuangs.urls).toHaveProperty('dict');
13
- expect(yuangs.urls).toHaveProperty('pong');
14
- expect(yuangs.urls.shici).toContain('shici/index.html');
15
- });
16
-
17
- test('should have openApp function', () => {
18
- expect(typeof yuangs.openApp).toBe('function');
19
- });
20
-
21
- test('should have backward compatibility functions', () => {
22
- expect(typeof yuangs.openShici).toBe('function');
23
- expect(typeof yuangs.openDict).toBe('function');
24
- expect(typeof yuangs.openPong).toBe('function');
25
- });
26
-
27
- test('should manage conversation history correctly', () => {
28
- yuangs.addToConversationHistory('user', 'hello');
29
- let history = yuangs.getConversationHistory();
30
- expect(history).toHaveLength(1);
31
- expect(history[0]).toEqual({ role: 'user', content: 'hello' });
32
-
33
- yuangs.addToConversationHistory('assistant', 'hi');
34
- history = yuangs.getConversationHistory();
35
- expect(history).toHaveLength(2);
36
- expect(history[1]).toEqual({ role: 'assistant', content: 'hi' });
37
- });
38
-
39
- test('should limit conversation history to 20 items', () => {
40
- for (let i = 0; i < 25; i++) {
41
- yuangs.addToConversationHistory('user', `msg ${i}`);
42
- }
43
- const history = yuangs.getConversationHistory();
44
- expect(history).toHaveLength(20);
45
- expect(history[history.length - 1].content).toBe('msg 24');
46
- // The first 5 should be dropped, so the first one in history should be 'msg 5'
47
- expect(history[0].content).toBe('msg 5');
48
- });
49
-
50
- test('should clear conversation history', () => {
51
- yuangs.addToConversationHistory('user', 'test');
52
- yuangs.clearConversationHistory();
53
- expect(yuangs.getConversationHistory()).toHaveLength(0);
54
- });
55
- });
56
-
57
- describe('CLI Integration', () => {
58
- const cliPath = path.join(__dirname, '../cli.js');
59
-
60
- test('should print help message', (done) => {
61
- exec(`node ${cliPath} --help`, (error, stdout, stderr) => {
62
- expect(error).toBeNull();
63
- expect(stdout).toContain('苑广山的个人应用启动器');
64
- expect(stdout).toContain('使用方法:');
65
- done();
66
- });
67
- });
68
-
69
- test('should list apps', (done) => {
70
- exec(`node ${cliPath} list`, (error, stdout, stderr) => {
71
- expect(error).toBeNull();
72
- expect(stdout).toContain('苑广山的应用列表');
73
- expect(stdout).toContain('shici');
74
- expect(stdout).toContain('dict');
75
- done();
76
- });
77
- });
78
- });
@@ -1,91 +0,0 @@
1
- const fs = require('fs');
2
- const yuangs = require('../index.js');
3
- const path = require('path');
4
- const os = require('os');
5
-
6
- jest.mock('fs');
7
-
8
- describe('Module: Macros', () => {
9
- const mockMacrosFile = path.join(os.homedir(), '.yuangs_macros.json');
10
-
11
- beforeEach(() => {
12
- jest.clearAllMocks();
13
- // Setup default mock implementation
14
- fs.existsSync.mockReturnValue(false);
15
- fs.readFileSync.mockReturnValue('{}');
16
- fs.writeFileSync.mockReturnValue(undefined);
17
- // We need to unmock path and os if they were mocked, but we only mocked fs
18
- });
19
-
20
- test('should get empty macros when file does not exist', () => {
21
- fs.existsSync.mockReturnValue(false);
22
- const macros = yuangs.getMacros();
23
- expect(macros).toEqual({});
24
- expect(fs.existsSync).toHaveBeenCalledWith(mockMacrosFile);
25
- });
26
-
27
- test('should save a new macro', () => {
28
- fs.existsSync.mockReturnValue(false); // File doesn't exist yet
29
-
30
- const result = yuangs.saveMacro('test', 'echo hello', 'description');
31
-
32
- expect(result).toBe(true);
33
- expect(fs.writeFileSync).toHaveBeenCalled();
34
-
35
- const [filePath, content] = fs.writeFileSync.mock.calls[0];
36
- expect(filePath).toBe(mockMacrosFile);
37
-
38
- const data = JSON.parse(content);
39
- expect(data).toHaveProperty('test');
40
- expect(data.test.commands).toBe('echo hello');
41
- expect(data.test.description).toBe('description');
42
- expect(data.test).toHaveProperty('createdAt');
43
- });
44
-
45
- test('should retrieve existing macros', () => {
46
- const mockData = {
47
- "demo": {
48
- "commands": "ls -la",
49
- "description": "list files"
50
- }
51
- };
52
- fs.existsSync.mockReturnValue(true);
53
- fs.readFileSync.mockReturnValue(JSON.stringify(mockData));
54
-
55
- const macros = yuangs.getMacros();
56
- expect(macros).toEqual(mockData);
57
- });
58
-
59
- test('should delete a macro', () => {
60
- const mockData = {
61
- "todelete": { "commands": "rm -rf /" },
62
- "keep": { "commands": "echo safe" }
63
- };
64
- fs.existsSync.mockReturnValue(true);
65
- fs.readFileSync.mockReturnValue(JSON.stringify(mockData));
66
-
67
- const result = yuangs.deleteMacro('todelete');
68
-
69
- expect(result).toBe(true);
70
- expect(fs.writeFileSync).toHaveBeenCalled();
71
-
72
- const [filePath, content] = fs.writeFileSync.mock.calls[0];
73
- const savedData = JSON.parse(content);
74
- expect(savedData).not.toHaveProperty('todelete');
75
- expect(savedData).toHaveProperty('keep');
76
- });
77
-
78
- test('should return false when deleting non-existent macro', () => {
79
- fs.existsSync.mockReturnValue(false); // Or true with empty object
80
-
81
- const result = yuangs.deleteMacro('nonexistent');
82
- expect(result).toBe(false);
83
- // Should not write to disk if nothing changed (optional optimization, but current implementation reads first)
84
- // Actually current implementation:
85
- // const macros = getMacros();
86
- // if (macros[name]) { ... }
87
- // getMacros returns {} if file not exists. macros['nonexistent'] is undefined.
88
- // So it returns false and does NOT call writeFileSync.
89
- expect(fs.writeFileSync).not.toHaveBeenCalled();
90
- });
91
- });
@@ -1,11 +0,0 @@
1
- {
2
- "shici": "https://wealth.want.biz/shici/index.html",
3
- "dict": "https://wealth.want.biz/pages/dict.html",
4
- "pong": "https://wealth.want.biz/pages/pong.html",
5
- "github": "https://github.com",
6
- "calendar": "https://calendar.google.com",
7
- "mail": "https://mail.google.com",
8
- "aiProxyUrl": "https://aiproxy.want.biz/v1/chat/completions",
9
- "defaultModel": "Assistant",
10
- "accountType": "free"
11
- }
@@ -1,23 +0,0 @@
1
- # Example configuration file for yuangs CLI
2
- # Add your custom applications here
3
-
4
- shici: "https://wealth.want.biz/shici/index.html"
5
- dict: "https://wealth.want.biz/pages/dict.html"
6
- pong: "https://wealth.want.biz/pages/pong.html"
7
- github: "https://github.com"
8
- calendar: "https://calendar.google.com"
9
- mail: "https://mail.google.com"
10
-
11
- # AI Configuration
12
- aiProxyUrl: "https://aiproxy.want.biz/v1/chat/completions"
13
- defaultModel: "Assistant"
14
- accountType: "free"
15
-
16
- # You can also use the apps property if you prefer to group them
17
- # apps:
18
- # shici: "https://wealth.want.biz/shici/index.html"
19
- # dict: "https://wealth.want.biz/pages/dict.html"
20
- # pong: "https://wealth.want.biz/pages/pong.html"
21
- # github: "https://github.com"
22
- # calendar: "https://calendar.google.com"
23
- # mail: "https://mail.google.com"
@@ -1,9 +0,0 @@
1
- {
2
- "shici": "https://wealth.want.biz/shici/index.html",
3
- "dict": "https://wealth.want.biz/pages/dict.html",
4
- "pong": "https://wealth.want.biz/pages/pong.html",
5
- "mail": "https://mail.google.com",
6
- "github": "https://github.com",
7
- "calendar": "https://calendar.google.com",
8
- "homepage": "https://i.want.biz"
9
- }