skill-os 0.1.0 → 0.1.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skill-os",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Skill-OS CLI for managing OS skills",
5
5
  "main": "skill-os.js",
6
6
  "bin": {
@@ -27,4 +27,4 @@
27
27
  "engines": {
28
28
  "node": ">=14.0.0"
29
29
  }
30
- }
30
+ }
package/skill-os.js CHANGED
@@ -61,20 +61,78 @@ function loadIndex() {
61
61
  return JSON.parse(raw);
62
62
  }
63
63
 
64
+ // ----------------------------------------------------------------------
65
+ // API Configuration
66
+ // ----------------------------------------------------------------------
67
+
68
+ const DEFAULT_API_BASE = "https://oscopilot.alibaba-inc.com/skills/api/v1";
69
+
70
+ function getApiBase(options) {
71
+ let url = options?.url || process.env.SKILL_OS_REGISTRY || DEFAULT_API_BASE;
72
+ return url.replace(/\/$/, "");
73
+ }
74
+
75
+ async function fetchFromApi(endpoint, options = {}) {
76
+ // Only require node-fetch dynamically when needed
77
+ let fetchFn = require('node-fetch');
78
+ if (typeof fetchFn !== 'function' && fetchFn.default) {
79
+ fetchFn = fetchFn.default;
80
+ }
81
+
82
+ let baseUrl;
83
+ // Extract url option if passed inside options object
84
+ if (options && options.url) {
85
+ baseUrl = getApiBase({ url: options.url });
86
+ delete options.url;
87
+ } else {
88
+ baseUrl = getApiBase({});
89
+ }
90
+
91
+ const url = `${baseUrl}${endpoint}`;
92
+
93
+ try {
94
+ const response = await fetchFn(url, options);
95
+ if (!response.ok) {
96
+ throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
97
+ }
98
+ return await response.json();
99
+ } catch (error) {
100
+ console.error(chalk.red(`\n✗ Error connecting to remote registry at ${url}`));
101
+ console.error(chalk.dim(error.message));
102
+ process.exit(1);
103
+ }
104
+ }
105
+
64
106
  // ----------------------------------------------------------------------
65
107
  // Commmands Implementations
66
108
  // ----------------------------------------------------------------------
67
109
 
68
- function cmdList() {
69
- const index = loadIndex();
70
- const skills = index.skills || {};
110
+ async function cmdList(options) {
111
+ console.log(`\n${chalk.dim('Fetching skills from external registry...')}`);
112
+ const index = await fetchFromApi('/skills', options);
113
+
114
+ // API returns an array of skills, or an object with a data/skills array property
115
+ let skillsList = [];
116
+ if (Array.isArray(index)) {
117
+ skillsList = index;
118
+ } else if (Array.isArray(index.data)) {
119
+ skillsList = index.data;
120
+ } else if (Array.isArray(index.skills)) {
121
+ skillsList = index.skills;
122
+ } else if (typeof index === 'object') {
123
+ // Fallback if it is an object map
124
+ skillsList = Object.entries(index.skills || index).map(([k, v]) => ({ path: k, ...v }));
125
+ }
71
126
 
72
127
  console.log(`\n${chalk.bold('📚 Skill-OS Available Skills')}`);
73
128
  console.log(`${chalk.dim('─'.repeat(60))}\n`);
74
129
 
75
130
  const layers = {};
76
- for (const [skillPath, info] of Object.entries(skills)) {
77
- const layer = skillPath.split('/')[0];
131
+ for (const info of skillsList) {
132
+ // Fallback path resolution. If the API returns 'path', use it. Otherwise, construct it or use name.
133
+ const skillPath = info.path || (info.layer ? `${info.layer}/${info.category || 'misc'}/${info.name}` : info.name || 'unknown');
134
+ const layer = info.layer || (skillPath.includes('/') ? skillPath.split('/')[0] : 'misc');
135
+
78
136
  if (!layers[layer]) layers[layer] = [];
79
137
  layers[layer].push({ path: skillPath, info });
80
138
  }
@@ -99,16 +157,22 @@ function cmdList() {
99
157
  }
100
158
  }
101
159
 
102
- function cmdSearch(query) {
103
- const lowercaseQuery = query.toLowerCase();
104
- const index = loadIndex();
105
- const skills = index.skills || {};
160
+ async function cmdSearch(query, options) {
161
+ console.log(`\n${chalk.dim(`Searching remote registry for '${query}'...`)}`);
162
+
163
+ // API Endpoint: /api/v1/search?q={{skill.name}}
164
+ const encodedQuery = encodeURIComponent(query);
165
+ const response = await fetchFromApi(`/search?q=${encodedQuery}`, options);
106
166
 
107
- const matches = [];
108
- for (const [skillPath, info] of Object.entries(skills)) {
109
- const searchable = `${skillPath} ${info.name || ''} ${info.description || ''}`.toLowerCase();
110
- if (searchable.includes(lowercaseQuery)) {
111
- matches.push({ path: skillPath, info });
167
+ // The API likely returns an array or an object map. Normalize to an array of objects.
168
+ let matches = [];
169
+ const rawResults = response.data || response.results || response.skills || response || [];
170
+
171
+ if (Array.isArray(rawResults)) {
172
+ matches = rawResults;
173
+ } else if (typeof rawResults === 'object') {
174
+ for (const [skillPath, info] of Object.entries(rawResults)) {
175
+ matches.push({ path: skillPath, ...info });
112
176
  }
113
177
  }
114
178
 
@@ -120,50 +184,45 @@ function cmdSearch(query) {
120
184
  console.log(`\n${chalk.bold(`🔍 Search Results for '${query}'`)}`);
121
185
  console.log(`${chalk.dim(`Found ${matches.length} skill(s)`)}\n`);
122
186
 
123
- for (const { path: skillPath, info } of matches) {
187
+ for (const info of matches) {
188
+ const skillPath = info.path || info.name; // Fallback to name if path isn't provided separately
124
189
  const icon = formatLayerIcon(skillPath);
125
190
  const status = formatStatus(info.status);
126
- const name = info.name || skillPath;
127
191
  const version = info.version || '?';
128
192
  const desc = info.description || '';
129
193
 
130
194
  console.log(`${icon} ${chalk.cyan(skillPath)}`);
131
- console.log(` ${chalk.bold(name)} (${version}) [${status}]`);
195
+ console.log(` ${chalk.bold(info.name)} (${version}) [${status}]`);
132
196
  console.log(` ${desc}\n`);
133
197
  }
134
198
  }
135
199
 
136
- function cmdInfo(skillPath) {
137
- const index = loadIndex();
138
- const skills = index.skills || {};
200
+ async function cmdInfo(skillPath, options) {
201
+ console.log(`\n${chalk.dim(`Fetching details from remote registry...`)}`);
139
202
 
140
- if (!skills[skillPath]) {
141
- console.error(chalk.red(`Error: Skill '${skillPath}' not found`));
142
- console.log(chalk.dim("Use 'skill-os list' to see available skills"));
143
- process.exit(1);
144
- }
203
+ // API Endpoint: /api/v1/skills/{{skill.path}}/content
204
+ const info = await fetchFromApi(`/skills/${skillPath}/content`, options);
145
205
 
146
- const info = skills[skillPath];
147
206
  const icon = formatLayerIcon(skillPath);
148
- const status = formatStatus(info.status);
207
+ const status = formatStatus(info.status || info.metadata?.status);
208
+ const metadata = info.metadata || info || {}; // Handle if nested or flat
149
209
 
150
- console.log(`\n${icon} ${chalk.bold(info.name || skillPath)}`);
210
+ console.log(`\n${icon} ${chalk.bold(metadata.name || skillPath)}`);
151
211
  console.log(chalk.dim('─'.repeat(40)));
152
212
  console.log(` Path: ${chalk.cyan(skillPath)}`);
153
- console.log(` Version: ${info.version || 'unknown'}`);
213
+ console.log(` Version: ${metadata.version || 'unknown'}`);
154
214
  console.log(` Status: ${status}`);
155
- console.log(` Description: ${info.description || 'No description'}`);
215
+ console.log(` Description: ${metadata.description || 'No description'}`);
156
216
 
157
- if (info.dependencies && info.dependencies.length > 0) {
158
- console.log(` Dependencies: ${info.dependencies.join(', ')}`);
217
+ if (metadata.dependencies && metadata.dependencies.length > 0) {
218
+ console.log(` Dependencies: ${metadata.dependencies.join(', ')}`);
159
219
  } else {
160
220
  console.log(` Dependencies: ${chalk.dim('None')}`);
161
221
  }
162
222
 
163
- const repoRoot = getRepoRoot();
164
- const skillMdPath = path.join(repoRoot, skillPath, 'SKILL.md');
165
- if (fs.existsSync(skillMdPath)) {
166
- console.log(`\n ${chalk.dim(`SKILL.md: ${skillMdPath}`)}`);
223
+ // If the API returns the actual markdown content, display a snippet or note
224
+ if (info.content || info.markdown) {
225
+ console.log(`\n ${chalk.dim('[Remote Document Content Available]')}`);
167
226
  }
168
227
  console.log();
169
228
  }
@@ -329,28 +388,38 @@ TODO: Add skill documentation here.
329
388
  console.log(` 3. Sync: ${chalk.dim(`skill-os sync ${skillPath}`)}`);
330
389
  }
331
390
 
332
- function cmdDownload(skillName, options) {
333
- const targetDir = options.platform
334
- ? path.join(process.cwd(), `.${options.platform}`, "skills")
335
- : process.cwd();
391
+ function cmdDownload(skillPath, options) {
392
+ // 1. Determine target directory
393
+ const homeDir = require('os').homedir();
394
+ let rawTarget = process.cwd();
336
395
 
337
- fs.mkdirSync(targetDir, { recursive: true });
396
+ if (options.target) {
397
+ rawTarget = options.target.startsWith('~/') ?
398
+ path.join(homeDir, options.target.slice(2)) :
399
+ path.resolve(options.target);
400
+ } else if (options.platform) {
401
+ rawTarget = path.join(process.cwd(), `.${options.platform}`, "skills");
402
+ }
338
403
 
339
- let serverUrl = options.url || process.env.SKILL_OS_REGISTRY || "https://example.com/skills";
340
- serverUrl = serverUrl.replace(/\/$/, ""); // Trim trailing slash
404
+ const targetDir = path.resolve(rawTarget);
405
+ fs.mkdirSync(targetDir, { recursive: true });
341
406
 
342
- const downloadUrl = `${serverUrl}/${skillName}.tar.gz`;
407
+ // 2. Determine API source
408
+ const serverUrl = getApiBase(options);
409
+ const downloadUrl = `${serverUrl}/skills/${skillPath}/download`;
343
410
 
344
- console.log(`\n${chalk.bold(`📥 Downloading ${skillName}...`)}`);
411
+ console.log(`\n${chalk.bold(`📥 Downloading and extracting ${skillPath}...`)}`);
345
412
  console.log(` Source: ${chalk.cyan(downloadUrl)}`);
346
413
  console.log(` Target: ${chalk.cyan(targetDir)}\n`);
347
414
 
348
415
  try {
349
- // Using curl synchronously for simplicity, similar to the bash version
350
- execSync(`curl -L -O ${downloadUrl}`, { cwd: targetDir, stdio: 'inherit' });
351
- console.log(`\n${chalk.green(`✓ Successfully downloaded ${skillName} to ${targetDir}`)}`);
416
+ // Stream the curl output directly into tar to extract the directory structure
417
+ // -s: silent curl, -L: follow redirects
418
+ // tar -xzf -: extract gzipped tar from stdin
419
+ execSync(`curl -s -L "${downloadUrl}" | tar -xzf -`, { cwd: targetDir, stdio: 'inherit' });
420
+ console.log(`\n${chalk.green(`✓ Successfully downloaded and extracted to: ${targetDir}`)}`);
352
421
  } catch (e) {
353
- console.error(`\n${chalk.red(`✗ Failed to download ${skillName}: command returned error code ${e.status}`)}`);
422
+ console.error(`\n${chalk.red(`✗ Failed to download. The API may have returned an error instead of a tarball.`)}`);
354
423
  process.exit(1);
355
424
  }
356
425
  }
@@ -771,37 +840,41 @@ program
771
840
  .description('Skill-OS CLI');
772
841
 
773
842
  program.command('list')
774
- .description('List all available skills')
843
+ .description('List all available skills from the remote registry')
844
+ .option('--url <url>', 'Override the base URL for the registry', DEFAULT_API_BASE)
775
845
  .action(cmdList);
776
846
 
777
847
  program.command('search')
778
- .description('Search for skills')
848
+ .description('Search for skills in the remote registry')
779
849
  .argument('<query>', 'Search query')
850
+ .option('--url <url>', 'Override the base URL for the registry', DEFAULT_API_BASE)
780
851
  .action(cmdSearch);
781
852
 
782
853
  program.command('info')
783
- .description('Show skill details')
784
- .argument('<path>', 'Skill path (e.g., package/package/rpm_search)')
854
+ .description('Show skill details from the remote registry')
855
+ .argument('<path>', 'Skill path (e.g., package/rpm_search)')
856
+ .option('--url <url>', 'Override the base URL for the registry', DEFAULT_API_BASE)
785
857
  .action(cmdInfo);
786
858
 
787
859
  program.command('install')
788
- .description('Install a skill to target directory')
860
+ .description('Install a skill to target directory locally from repository')
789
861
  .argument('<path>', 'Skill path to install')
790
862
  .option('-t, --target <dir>', 'Target directory', '~/.skills')
791
863
  .option('-f, --force', 'Force overwrite if exists', false)
792
864
  .action(cmdInstall);
793
865
 
794
866
  program.command('create')
795
- .description('Create a new skill scaffold')
867
+ .description('Create a new skill scaffold locally')
796
868
  .argument('<path>', 'Skill path to create (e.g., system/migration)')
797
869
  .option('-f, --force', 'Force overwrite if exists', false)
798
870
  .action(cmdCreate);
799
871
 
800
872
  program.command('download')
801
- .description('Download a skill package')
802
- .argument('<skill_name>', 'Name of the skill to download')
873
+ .description('Download and extract a skill package from the remote registry')
874
+ .argument('<skill_path>', 'Path of the skill to download (e.g., core/kernel/kernel-info)')
875
+ .option('-t, --target <dir>', 'Target directory to extract into (defaults to current dir)')
803
876
  .option('--platform <platform>', 'Specify the platform (e.g., qoder will download to .qoder/skills/)')
804
- .option('--url <url>', 'Specify the base URL for the registry (defaults to SKILL_OS_REGISTRY env var)')
877
+ .option('--url <url>', 'Override the base URL for the registry', DEFAULT_API_BASE)
805
878
  .action(cmdDownload);
806
879
 
807
880
  program.command('sync')
@@ -0,0 +1,97 @@
1
+ ---
2
+ name: rpm-find
3
+ version: 1.0.0
4
+ description: 专业的 Alinux 系统 RPM 软件包搜索工具。支持解析复杂的包名查询,根据用户网络环境(内网/公网)智能提供有效下载链接。当用户请求查找、下载或查询 RPM 软件包时使用此技能。
5
+
6
+
7
+ layer: runtime
8
+ category: package
9
+ lifecycle: usage
10
+
11
+ # Tags: first tag MUST be the category name
12
+ tags:
13
+ - package
14
+ - rpm
15
+ - alinux
16
+ - search
17
+
18
+ status: stable
19
+ dependencies:
20
+ - httpx>=0.24.0
21
+ - requests>=2.28.0
22
+ ---
23
+
24
+ # RPM 软件包搜索技能
25
+
26
+ 当用户请求查找、下载或查询 Alinux 系统的 RPM 软件包时使用此技能。
27
+
28
+ ## 能力概览
29
+
30
+ 此技能能够解析模糊的包名(如 "nginx", "kernel 5.10"),并调用工具搜索实际的下载地址。它能根据用户的身份和网络环境,动态调整搜索策略和回复语气。
31
+
32
+ ## 核心流程
33
+
34
+ ### 步骤 1: 知识库加载 (必选)
35
+
36
+ 在开始任何搜索之前,必须使用 `read_file` 工具读取以下文档以了解 Alinux 的内核与版本命名规则:
37
+
38
+ - [Alinux 内核与版本识别](./references/kernel_kb.md)
39
+
40
+ ### 步骤 2: 意图识别与扩展 (可选分支)
41
+
42
+ 在进行常规搜索前,请检查用户的具体意图。如果用户明确咨询 **"debuginfo"**, **"崩溃调试"**, **"内核调试"**, 或 **"debug 仓库"** 相关问题:
43
+
44
+ - **加载**: [Alinux Debuginfo 包查找指南](./references/debuginfo_guide.md)
45
+ - **执行**: 根据该指南中的 "标准用法" 或 "URL拼接" 规则回答用户的问题。
46
+
47
+ ### 步骤 3: 环境判断与 SOP 加载 (分支)
48
+
49
+ 请根据用户的 `source` 字段或当前对话上下文判断用户环境,并加载对应的 SOP 文档:
50
+
51
+ - **情形 A: 内部开发环境** (例如 `source` 为 `cardbot_in` 或提到 "内部源")
52
+ - 加载: [内网 RPM 搜索 SOP](./references/inner_rules.md)
53
+ - 执行重点: 优先使用内网源,不过滤闭源包,回复简洁。
54
+
55
+ - **情形 B: 外部公网环境** (默认情况,或 `source` 为 `other`)
56
+ - 加载: [公网 RPM 搜索 SOP](./references/outer_rules.md)
57
+ - 执行重点: **严禁泄露内网 IP**,必须转换为 `mirrors.aliyun.com`,语气亲切。
58
+
59
+ ### 步骤 4: 工具调用 (CLI 模式)
60
+
61
+ 使用 `execute_shell` 运行 Python 脚本执行操作。
62
+
63
+ **脚本路径**: `./scripts/rpm_tool.py`
64
+
65
+ #### 4.1 解析查询
66
+
67
+ ```bash
68
+ uv run ./scripts/rpm_tool.py parse "kernel 5.10"
69
+ ```
70
+
71
+ #### 4.2 搜索 RPM (API)
72
+
73
+ 将解析结果 (JSON) 中的字段传入:
74
+
75
+ ```bash
76
+ uv run ./scripts/rpm_tool.py search "kernel" --version "5.10.134" --release "13.al8" --arch "x86_64"
77
+ ```
78
+
79
+ #### 4.3 验证 URL
80
+
81
+ 将找到的 URL 列表传入验证:
82
+
83
+ ```bash
84
+ uv run ./scripts/rpm_tool.py verify http://url1 http://url2 ...
85
+ ```
86
+
87
+ #### 4.4 备用: URL 拼接 (仅当 API 无结果时)
88
+
89
+ ```bash
90
+ uv run ./scripts/rpm_tool.py construct "kernel-debuginfo" "5.10.134" "13.al8" "x86_64" --is_kernel --alinux_version "3"
91
+ ```
92
+
93
+ ## 错误处理
94
+
95
+ - 如果 API 返回空结果,尝试使用 `construct` 命令拼接 URL
96
+ - 如果所有 URL 验证失败,建议用户检查包名或版本号是否正确
97
+ - 对于闭源包(含 `ali3000`, `ali4000` 等标识),提示用户联系 OS 团队
@@ -0,0 +1,12 @@
1
+ [project]
2
+ name = "package/rpm_search-skill"
3
+ version = "1.0.0"
4
+ description = "RPM package search skill for Alinux systems"
5
+ requires-python = ">=3.9"
6
+ dependencies = [
7
+ "httpx>=0.24",
8
+ "requests>=2.28",
9
+ ]
10
+
11
+ [project.scripts]
12
+ rpm-tool = "scripts.rpm_tool:main"
@@ -0,0 +1,29 @@
1
+ # Alinux Debuginfo 包查找指南
2
+
3
+ ## 标准用法 (YUM/DNF)
4
+ Alinux 系统找包的标准用法是通过 YUM/DNF 工具。对于 debuginfo 包,需要使用 `yum-utils` 提供的 `debuginfo-install` 命令,因为 debug 仓库默认未开启。
5
+
6
+ ### 常用命令
7
+ - **安装常规包**: `debuginfo-install <pkg>` (例如 `debuginfo-install tree`)
8
+ - **安装内核包**: `debuginfo-install kernel-debuginfo` (必须带后缀,否则安装不上)
9
+ - **仅下载不安装**: `debuginfo-install --downloadonly kernel-debuginfo-$(uname -r)`
10
+ - 下载位置: `/var/cache/dnf/`
11
+
12
+ ## URL 拼接用法 (不推荐但可用)
13
+ 仅在不得不手动下载时使用。**注意**: 仅适用于公网镜像 `mirrors.aliyun.com`。闭源组件 (`ali3000`等) 不适用此法。
14
+
15
+ ### 拼接规则
16
+ 1. **架构推断**:
17
+ - `.al7` -> Alinux 2
18
+ - `.al8` -> Alinux 3
19
+ - `.alnx4` -> Alinux 4
20
+ 2. **内核包 URL**:
21
+ - **Alinux 2/3**: `<dist>/plus/<arch>/debug/kernels/`
22
+ - **Alinux 4**: `<dist>/os/<arch>/debug/` 或 `<dist>/updates/<arch>/debug/`
23
+ - 文件名: `kernel-debuginfo-<version>-<release>.rpm` 和 `kernel-debuginfo-common-<arch>-<version>-<release>.rpm`
24
+ 3. **非内核包 URL**:
25
+ - 路径: `<dist>/<repo>/<arch>/debug/<pkg>-debuginfo-<version>-<release>.rpm`
26
+
27
+ ### 注意事项
28
+ - 如果需要查找 debuginfo,若是内核包,需同时提供 `kernel-debuginfo` 和 `kernel-debuginfo-common`。
29
+ - 若闭源包无 debuginfo,需联系 OS 团队。
@@ -0,0 +1,17 @@
1
+ # 内网 RPM 搜索 SOP (Inner)
2
+
3
+ **适用场景**: 用户身份为内部员工 (`source: cardbot_in`) 或检测到内网环境。
4
+
5
+ ## 核心准则
6
+ 你是一名顶级的 Alinux 系统 RPM 包管理专家智能体。
7
+ 1. **提供有效链接**: 必须提供 `yum.tbsite.net` 等内网源链接。
8
+ 2. **验证**: 所有链接必须先通过 `check_url_availability_batch` 验证。
9
+ 3. **语气**: 专业、直接、技术导向。
10
+
11
+ ## 执行步骤
12
+ 1. **解析**: 调用 `parse_user_query_to_rpm_info` 解析包名。
13
+ 2. **查询**: 调用 `get_rpm_list` 获取内网链接。
14
+ 3. **闭源包处理**:
15
+ - 如果发现 `apsara` 或 `aliXXXX` 等闭源包,**可以**显示,并标记为“内部闭源”。
16
+ 4. **验证**: 并行验证所有 URL。
17
+ 5. **输出**: 直接列出 Markdown 格式的下载链接。
@@ -0,0 +1,16 @@
1
+ # Alinux 内核与版本识别知识库
2
+
3
+ 本参考文档包含识别 Alinux 内核及对应系统版本的核心规则。
4
+
5
+ ## 内核识别规则
6
+ - **内核判断**: 版本号以 5.10, 6.6, 4.19, 4.9 等主线版本开头,或包含 `ali3000`, `ali4000`, `ali5000`, `ali6000`, `kernel`, `kmod` 字样,判定为内核。
7
+ - 注意:`ali5000`, `ali6000` 是内核标识符,而不是软件包名称。
8
+ - **内核包名**: 内核对应 `kernel`, 包名一般为 `kernel`, `kernel-devel`, `kernel-core`, `kernel-modules` 等。
9
+ - **闭源内核**: 包含 `ali3000`, `ali4000`, `ali5000`, `ali6000` 等字样, 或API返回的仓库路径包含 `apsara-xxxx` 等特殊标识, 代表内部闭源包。
10
+
11
+ ## 系统版本对应表
12
+ | 标识符 | Alinux 版本 | 备注 |
13
+ | :--- | :--- | :--- |
14
+ | `.al7` | Alinux 2 | |
15
+ | `.al8` | Alinux 3 | 内核主版本号 5.10 也对应 Alinux 3 |
16
+ | `.alnx4`| Alinux 4 | 内核主版本号 6.6 也对应 Alinux 4 |
@@ -0,0 +1,20 @@
1
+ # 公网 RPM 搜索 SOP (Outer)
2
+
3
+ **适用场景**: 用户身份为外部客户 (`source: other`) 或公网环境。
4
+
5
+ ## 核心准则
6
+ 你是一名服务于外部客户的 Alinux 系统专家。
7
+ 1. **只提供公网链接**: *绝对禁止* 泄露 `yum.tbsite.net` 等内网链接。必须转换为 `mirrors.aliyun.com`。
8
+ 2. **验证**: 所有链接必须通过 `check_url_availability_batch` 验证。
9
+ 3. **语气**: 亲切、耐心、解释性强。
10
+
11
+ ## 执行步骤
12
+ 1. **解析**: 调用 `parse_user_query_to_rpm_info`。
13
+ 2. **查询**: 调用 `get_rpm_list`。
14
+ 3. **安全过滤 (关键)**:
15
+ - 检查是否为闭源包 (`apsara`, `aliXXXX`)。若是,**绝对不能**提供链接,告知用户“需联系售后获取”。
16
+ - 将所有内网 URL (`http://yum.tbsite.net/...`) 替换为公网镜像 (`https://mirrors.aliyun.com/...`)。
17
+ 4. **兜底**: 如果转换失败,调用 `construct_url` 尝试拼接。
18
+ 5. **输出**:
19
+ - 使用表格形式展示结果。
20
+ - 对每个步骤做简单解释,帮助客户理解。
@@ -0,0 +1,417 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ RPM Search Tool Script
4
+ 此脚本封装了 RPM 搜索相关的核心功能,供 AI Agent 通过命令行调用。
5
+ """
6
+
7
+ import argparse
8
+ import asyncio
9
+ import json
10
+ import logging
11
+ import re
12
+
13
+ from typing import Any
14
+
15
+ import httpx
16
+ import requests
17
+
18
+
19
+ # 设置日志
20
+ logging.basicConfig(level=logging.ERROR, format="%(message)s")
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # --- Core Functions from example_agent.py ---
24
+
25
+
26
+ def fetch_webpage_content(
27
+ url: str, params: dict | None = None, timeout: int = 10
28
+ ) -> str:
29
+ """获取url返回的数据"""
30
+ try:
31
+ response = requests.get(url=url, params=params, timeout=timeout)
32
+ response.raise_for_status()
33
+ return response.text
34
+ except requests.exceptions.RequestException as e:
35
+ logger.error(f"网络请求失败: {e}")
36
+ return ""
37
+
38
+
39
+ def parse_user_query_to_rpm_info(query: str) -> list[dict[str, Any]]:
40
+ query = query.replace("内核", " kernel ")
41
+ cleaned_query = re.sub(r"[\u4e00-\u9fa5]", " ", query)
42
+ work_str = " ".join(cleaned_query.split())
43
+ package_queries = re.findall(
44
+ r"[\w\.~-]*?(?:\d{1,2}\.\d{1,2}|al[78]|alnx4|x86_64|aarch64)[\w\.~-]*",
45
+ work_str,
46
+ )
47
+ if not package_queries:
48
+ package_queries = [work_str]
49
+ results = []
50
+ for part in package_queries:
51
+ if not part or len(part) < 5:
52
+ continue
53
+ info = {
54
+ "name": "",
55
+ "version": "",
56
+ "release": "",
57
+ "arch": "",
58
+ "type": "standard",
59
+ "is_kernel": False,
60
+ "alinux_version": "unknown",
61
+ "original_text": part,
62
+ }
63
+ arch_match = re.search(r"\b(x86_64|aarch64)\b", part)
64
+ if arch_match:
65
+ info["arch"] = arch_match.group(1)
66
+ part = part.replace(arch_match.group(0), "").strip()
67
+ dist_match = re.search(r"[\.-](al[78]|alnx4)", part)
68
+ if dist_match:
69
+ info["alinux_version"] = {"al7": "2", "al8": "3", "alnx4": "4"}.get(
70
+ dist_match.group(1)
71
+ )
72
+ part = part[: dist_match.start()]
73
+ ver_match = re.search(r"(\d{1,2}\.\d{1,2}(?:\.\d{1,3})?)", part)
74
+ if ver_match:
75
+ name_candidate = part[: ver_match.start()].strip("-_ ")
76
+ version_release_candidate = part[ver_match.start() :]
77
+ vr_parts = version_release_candidate.split("-", 1)
78
+ info["version"] = vr_parts[0]
79
+ if len(vr_parts) > 1:
80
+ info["release"] = vr_parts[1]
81
+ else:
82
+ final_vr_parts = info["version"].rsplit("-", 1)
83
+ if len(final_vr_parts) == 2:
84
+ info["version"] = final_vr_parts[0]
85
+ info["release"] = final_vr_parts[1]
86
+ info["name"] = name_candidate
87
+ else:
88
+ name_ver_parts = part.rsplit("-", 1)
89
+ if len(name_ver_parts) == 2 and re.match(r"\d", name_ver_parts[1]):
90
+ info["name"] = name_ver_parts[0]
91
+ ver_rel_parts = name_ver_parts[1].rsplit("-", 1)
92
+ info["version"] = ver_rel_parts[0]
93
+ if len(ver_rel_parts) > 1:
94
+ info["release"] = ver_rel_parts[1]
95
+ else:
96
+ info["name"] = part
97
+ final_name = info["name"].replace(".rpm", "").strip("-_ ")
98
+ if not final_name and ("kernel" in work_str or "vmlinux" in work_str):
99
+ final_name = "kernel"
100
+ kernel_markers = [
101
+ "ali3000",
102
+ "ali4000",
103
+ "ali5000",
104
+ "ali6000",
105
+ "kernel",
106
+ "kmod",
107
+ ]
108
+ if any(marker in final_name for marker in kernel_markers) or (
109
+ ver_match and ver_match.group(1).startswith(("5.10", "4.19", "6.6"))
110
+ ):
111
+ info["is_kernel"] = True
112
+ if info["alinux_version"] == "unknown":
113
+ if info.get("release") and "al8" in info["release"]:
114
+ info["alinux_version"] = "3"
115
+ elif info.get("release") and "al7" in info["release"]:
116
+ info["alinux_version"] = "2"
117
+ elif info.get("release") and "alnx4" in info["release"]:
118
+ info["alinux_version"] = "4"
119
+ elif info["is_kernel"] and info.get("version", "").startswith(
120
+ "5.10"
121
+ ):
122
+ info["alinux_version"] = "3"
123
+ elif info["is_kernel"] and info.get("version", "").startswith(
124
+ "6.6"
125
+ ):
126
+ info["alinux_version"] = "4"
127
+ if (
128
+ "debug" in work_str
129
+ or "vmlinux" in work_str
130
+ or "debuginfo" in final_name
131
+ ):
132
+ info["type"] = "debuginfo"
133
+ if info["type"] == "debuginfo" and "debuginfo" not in final_name:
134
+ final_name += "-debuginfo"
135
+ info["name"] = final_name.strip("-")
136
+ if info["name"]:
137
+ results.append(info)
138
+ return results
139
+
140
+
141
+ # 全局共享的AsyncClient实例,配置连接池以支持高并发
142
+ _shared_http_client: httpx.AsyncClient | None = None
143
+
144
+
145
+ def _get_shared_http_client() -> httpx.AsyncClient:
146
+ """获取共享的httpx AsyncClient实例,配置连接池支持高并发"""
147
+ global _shared_http_client
148
+ if _shared_http_client is None:
149
+ # 配置连接池限制,支持高并发请求
150
+ limits = httpx.Limits(
151
+ max_connections=200, # 最大连接数
152
+ max_keepalive_connections=50, # 保持活动的连接数
153
+ )
154
+ _shared_http_client = httpx.AsyncClient(
155
+ limits=limits,
156
+ timeout=1.5,
157
+ follow_redirects=True,
158
+ )
159
+ return _shared_http_client
160
+
161
+
162
+ async def _check_single_url(client: httpx.AsyncClient, url: str) -> dict:
163
+ """
164
+ 异步检查单个URL的可用性
165
+ """
166
+ headers = {
167
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
168
+ }
169
+ result = {"url": url}
170
+ try:
171
+ response = await client.head(url, headers=headers)
172
+ if response.status_code == 200:
173
+ result["status"] = "valid"
174
+ result["status_code"] = "200"
175
+ else:
176
+ result["status"] = "invalid"
177
+ result["status_code"] = str(response.status_code)
178
+ except httpx.TimeoutException:
179
+ result["status"] = "error"
180
+ result["reason"] = "Request timed out"
181
+ except httpx.ConnectError as e:
182
+ result["status"] = "error"
183
+ result["reason"] = f"Connection error: {e}"
184
+ except httpx.HTTPError as e:
185
+ result["status"] = "error"
186
+ result["reason"] = f"An unexpected error occurred: {e}"
187
+ return result
188
+
189
+
190
+ async def check_urls_availability_batch(
191
+ urls: list[str],
192
+ ) -> list[dict[str, Any]]:
193
+ """
194
+ 并发检查一批URLs是否有效且可访问。
195
+ """
196
+ if not urls:
197
+ return []
198
+
199
+ client = _get_shared_http_client()
200
+ # 使用asyncio.gather并发执行所有URL检查
201
+ tasks = [_check_single_url(client, url) for url in urls]
202
+ results = await asyncio.gather(*tasks)
203
+ return list(results)
204
+
205
+
206
+ def get_rpm_list(
207
+ name: str, version: str = "", release: str = "", arch: str = ""
208
+ ) -> list[dict[str, str]]:
209
+ """
210
+ 通过内部API查询RPM包的详细信息。返回结果是内网URL,需要后续处理。
211
+ """
212
+ base_url = "http://opsx.vip.tbsite.net/gapi/v1/cms/rpm/list?"
213
+ # 确保version包含2位小数点,避免精确查询失败
214
+ if version and version.count(".") != 2:
215
+ version = ""
216
+ params = {
217
+ "name": name,
218
+ "version": version,
219
+ "release": release,
220
+ "arch": arch,
221
+ }
222
+
223
+ rep_text = fetch_webpage_content(base_url, params=params)
224
+ if not rep_text:
225
+ return []
226
+
227
+ try:
228
+ rep_json = json.loads(rep_text)
229
+ rpms = rep_json.get("data", {}).get("rpms", [])
230
+ return [
231
+ {
232
+ "name": item.get("name", ""),
233
+ "version": item.get("version", ""),
234
+ "release": item.get("release", ""),
235
+ "arch": item.get("arch", ""),
236
+ "internal_url": item.get("download_url", ""),
237
+ }
238
+ for item in rpms
239
+ if "alinux" in item.get("download_url", "")
240
+ ]
241
+ except (json.JSONDecodeError, TypeError) as e:
242
+ logger.error(f"API响应解析失败: {e}")
243
+ return []
244
+
245
+
246
+ async def construct_url(
247
+ name: str,
248
+ version: str,
249
+ release: str,
250
+ arch: str,
251
+ is_kernel: bool = False,
252
+ alinux_version: str = "3",
253
+ ) -> list[str]:
254
+ """
255
+ 根据包信息拼接公网URL作为备用方案,name大小写敏感。
256
+ """
257
+ # 步骤1: 输入验证
258
+ if (
259
+ not all([name, version, release, arch, alinux_version])
260
+ or alinux_version == "unknown"
261
+ ):
262
+ return []
263
+
264
+ urls = []
265
+ full_rpm_filename = f"{name}-{version}-{release}.{arch}.rpm"
266
+
267
+ # 步骤2: 根据包类型选择不同的拼接逻辑
268
+ # 逻辑分支 A: 内核Debuginfo包
269
+ if is_kernel and "debuginfo" in name:
270
+ full_ver_rel = f"{version}-{release}"
271
+ if alinux_version in ["2", "3"]:
272
+ # Alinux 2/3 的 debuginfo 在 plus 仓库的特定子目录
273
+ base_url = f"https://mirrors.aliyun.com/alinux/{alinux_version}/plus/{arch}/debug/kernels"
274
+ urls.append(
275
+ f"{base_url}/kernel-debuginfo-{full_ver_rel}.{arch}.rpm"
276
+ )
277
+ # 内核debuginfo需要common包
278
+ urls.append(
279
+ f"{base_url}/kernel-debuginfo-common-{arch}-{version}-{release}.rpm"
280
+ )
281
+ elif alinux_version == "4":
282
+ # Alinux 4 可能在 os 或 updates 仓库
283
+ for repo in ["os", "updates"]:
284
+ base_url = f"https://mirrors.aliyun.com/alinux/{alinux_version}/{repo}/{arch}/debug"
285
+ urls.append(
286
+ f"{base_url}/kernel-debuginfo-{full_ver_rel}.{arch}.rpm"
287
+ )
288
+ urls.append(
289
+ f"{base_url}/kernel-debuginfo-common-{arch}-{version}-{release}.rpm"
290
+ )
291
+
292
+ # 逻辑分支 B: 普通RPM包
293
+ else:
294
+ # common_repos = ['os', 'plus', 'updates', 'powertools', 'extras']
295
+ common_repos = ["os"]
296
+ for repo in common_repos:
297
+ url = f"https://mirrors.aliyun.com/alinux/{alinux_version}/{repo}/{arch}/Packages/{full_rpm_filename}"
298
+ urls.append(url)
299
+ # 对于debuginfo包,有可能在debug目录下
300
+ if "debuginfo" in name:
301
+ url_debug_repo = f"https://mirrors.aliyun.com/alinux/{alinux_version}/{repo}/{arch}/debug/Packages/{full_rpm_filename}"
302
+ urls.append(url_debug_repo)
303
+ # 使用并发批量检查所有URL
304
+ client = _get_shared_http_client()
305
+ tasks = [_check_single_url(client, url) for url in urls]
306
+ check_results = await asyncio.gather(*tasks)
307
+
308
+ return [
309
+ result["url"]
310
+ for result in check_results
311
+ if result.get("status") == "valid"
312
+ ]
313
+
314
+
315
+ # --- CLI Wrappers ---
316
+
317
+
318
+ def cmd_parse(args):
319
+ results = parse_user_query_to_rpm_info(args.query)
320
+ print(json.dumps(results, ensure_ascii=False, indent=2))
321
+
322
+
323
+ def cmd_search(args):
324
+ results = get_rpm_list(
325
+ name=args.name,
326
+ version=args.version,
327
+ release=args.release,
328
+ arch=args.arch,
329
+ )
330
+ print(json.dumps(results, ensure_ascii=False, indent=2))
331
+
332
+
333
+ async def async_cmd_verify(args):
334
+ urls = args.urls
335
+ results = await check_urls_availability_batch(urls)
336
+ print(json.dumps(results, ensure_ascii=False, indent=2))
337
+
338
+
339
+ def cmd_verify(args):
340
+ asyncio.run(async_cmd_verify(args))
341
+
342
+
343
+ async def async_cmd_construct(args):
344
+ results = await construct_url(
345
+ name=args.name,
346
+ version=args.version,
347
+ release=args.release,
348
+ arch=args.arch,
349
+ is_kernel=args.is_kernel,
350
+ alinux_version=args.alinux_version,
351
+ )
352
+ print(json.dumps(results, ensure_ascii=False, indent=2))
353
+
354
+
355
+ def cmd_construct(args):
356
+ asyncio.run(async_cmd_construct(args))
357
+
358
+
359
+ def main():
360
+ parser = argparse.ArgumentParser(description="RPM Search Tool CLI")
361
+ subparsers = parser.add_subparsers(
362
+ dest="command", help="Available commands"
363
+ )
364
+
365
+ # Command: parse
366
+ parser_parse = subparsers.add_parser(
367
+ "parse", help="Parse user query to RPM info"
368
+ )
369
+ parser_parse.add_argument("query", type=str, help="User query string")
370
+ parser_parse.set_defaults(func=cmd_parse)
371
+
372
+ # Command: search
373
+ parser_search = subparsers.add_parser("search", help="Search RPM via API")
374
+ parser_search.add_argument("name", type=str, help="Package name")
375
+ parser_search.add_argument(
376
+ "--version", type=str, default="", help="Package version"
377
+ )
378
+ parser_search.add_argument(
379
+ "--release", type=str, default="", help="Package release"
380
+ )
381
+ parser_search.add_argument(
382
+ "--arch", type=str, default="", help="Architecture"
383
+ )
384
+ parser_search.set_defaults(func=cmd_search)
385
+
386
+ # Command: verify
387
+ parser_verify = subparsers.add_parser(
388
+ "verify", help="Verify URLs availability"
389
+ )
390
+ parser_verify.add_argument("urls", nargs="+", help="List of URLs to verify")
391
+ parser_verify.set_defaults(func=cmd_verify)
392
+
393
+ # Command: construct
394
+ parser_construct = subparsers.add_parser(
395
+ "construct", help="Construct URL fallback"
396
+ )
397
+ parser_construct.add_argument("name", type=str, help="Package name")
398
+ parser_construct.add_argument("version", type=str, help="Package version")
399
+ parser_construct.add_argument("release", type=str, help="Package release")
400
+ parser_construct.add_argument("arch", type=str, help="Architecture")
401
+ parser_construct.add_argument(
402
+ "--is_kernel", action="store_true", help="Is kernel package"
403
+ )
404
+ parser_construct.add_argument(
405
+ "--alinux_version", type=str, default="3", help="Alinux version"
406
+ )
407
+ parser_construct.set_defaults(func=cmd_construct)
408
+
409
+ args = parser.parse_args()
410
+ if hasattr(args, "func"):
411
+ args.func(args)
412
+ else:
413
+ parser.print_help()
414
+
415
+
416
+ if __name__ == "__main__":
417
+ main()