openclawmp 1.0.2 → 1.0.3

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/README.md CHANGED
@@ -21,6 +21,9 @@ openclawmp oauth <your-token>
21
21
  # 搜索市场资产
22
22
  openclawmp search 天气
23
23
 
24
+ # 搜索用户
25
+ openclawmp users 张三
26
+
24
27
  # 根据 assetId 查看资产详情
25
28
  openclawmp info 6c476a36955a4270a3fa303beeeed5ee
26
29
 
@@ -40,6 +43,7 @@ openclawmp publish ./my-asset --yes
40
43
  ## 功能概览
41
44
 
42
45
  - `search`:搜索市场资产,按类型、作者、安装量、标签和简介格式化输出。
46
+ - `users`:搜索用户,默认展示前 10 条,并显示总结果数与下一页游标。
43
47
  - `oauth`:将 token 写入本地凭证文件。
44
48
  - `info`:根据 `assetId` 查看资产详情与版本信息。
45
49
  - `install`:根据 `assetId` 和可选 `semver` 下载归档并安装到本地 OpenClaw 目录。
@@ -154,6 +158,7 @@ openclawmp uninstall skill/demo-skill
154
158
 
155
159
  ```bash
156
160
  openclawmp search 天气 --page-size 10
161
+ openclawmp users openclawmp.cc
157
162
  ```
158
163
 
159
164
  ## 命令结构
@@ -163,12 +168,13 @@ openclawmp search 天气 --page-size 10
163
168
  - `publish`
164
169
  - `oauth`
165
170
  - `search`
171
+ - `users`
166
172
  - `info`
167
173
  - `install`
168
174
  - `uninstall`
169
175
  - `list`
170
176
 
171
- 其中当前可用的是 `publish`、`oauth`、`search`、`info`、`install`、`uninstall`、`list`。
177
+ 其中当前可用的是 `publish`、`oauth`、`search`、`users`、`info`、`install`、`uninstall`、`list`。
172
178
 
173
179
  ## 发布命令参数
174
180
 
@@ -181,33 +187,3 @@ openclawmp search 天气 --page-size 10
181
187
  - `--dry-run`
182
188
  - `--yes, -y`
183
189
  - `--verbose`
184
-
185
- ## 目录发布打包规则
186
-
187
- - 目录发布会默认跳过 `.git`、`node_modules`、`__MACOSX`、`.DS_Store`、`Thumbs.db`
188
- - 根目录可通过 `.openclawmpignore` 补充忽略规则,也会读取根目录 `.gitignore` / `.npmignore` 的常见规则
189
- - 子路径遇到无权限读取的目录或文件时会自动跳过;使用 `--verbose` 可查看具体跳过项
190
-
191
- ## Publish Metadata
192
-
193
- - `publish` 只读取发布目录或归档里的 `.metadata.json`
194
- - `.metadata.json` 必须包含 `assetType`、`name`、`displayName`、`semver`
195
- - `semver` 必须是严格的 `x.y.z` 纯数字格式,例如 `1.2.3`
196
- - 可选字段:`assetId`、`category`、`tags`、`description`、`longDescription`
197
- - 不再从 `README.md`、`package.json`、`openclaw.plugin.json`、`SKILL.md` 自动推断发布 metadata
198
- - 不再通过 CLI 参数补录 `assetType`、`name`、`displayName`、`semver`
199
- - `assetId` 可写在 `.metadata.json`,也可以通过 `--asset-id` 或目录根部 `.assetid` 透传
200
- - `objectId` 由上传完成后自动生成,登录态始终必需
201
- - 默认不打印请求预览或请求头明细;加 `--verbose` 会额外输出接口 URL、入参与返回详情
202
-
203
- ## 默认配置来源
204
-
205
- `baseUrl` 优先级:
206
-
207
- 1. `--base-url`
208
- 2. `OPENCLAWMP_API_BASE_URL`
209
- 3. `OPENCLAWMP_BASE_URL`
210
- 4. `API_BASE_URL`
211
- 5. `NEXT_PUBLIC_API_BASE_URL`
212
- 6. 仓库根目录 `.env.local` / `.env` / `.env.prod`
213
- 7. `https://seafood.c.stepfun-inc.net`
package/bin/openclawmp.js CHANGED
@@ -24,6 +24,9 @@ const COMMAND_LOADERS = {
24
24
  search: function loadSearch() {
25
25
  return require("../lib/commands/search");
26
26
  },
27
+ users: function loadUsers() {
28
+ return require("../lib/commands/users");
29
+ },
27
30
  info: function loadInfo() {
28
31
  return require("../lib/commands/info");
29
32
  },
package/lib/api.js CHANGED
@@ -10,6 +10,7 @@ const {
10
10
  } = require("./utils");
11
11
 
12
12
  const CATALOG_SERVICE = "step.seafood.catalog.CatalogService";
13
+ const IDENTITY_SERVICE = "step.seafood.identity.IdentityService";
13
14
 
14
15
  class ApiError extends Error {
15
16
  constructor(message, details) {
@@ -136,10 +137,10 @@ function createCatalogClient(options) {
136
137
  const defaultRequestId = normalizeRequestId(options.requestId) || generateRequestId();
137
138
  const verbose = Boolean(options.verbose);
138
139
 
139
- async function unary(methodName, body, extra) {
140
+ async function unaryForService(serviceName, methodName, body, extra) {
140
141
  const requestId = normalizeRequestId((extra && extra.requestId) || defaultRequestId);
141
142
  const operationName = humanizeApiMethodName(methodName) || methodName || "接口调用";
142
- const requestUrl = String(buildServiceUrl(options.baseUrl, CATALOG_SERVICE, methodName));
143
+ const requestUrl = String(buildServiceUrl(options.baseUrl, serviceName, methodName));
143
144
  const requestBody = JSON.stringify(body || {}, jsonReplacer);
144
145
  const headers = {
145
146
  "Connect-Protocol-Version": "1",
@@ -233,10 +234,17 @@ function createCatalogClient(options) {
233
234
  };
234
235
  }
235
236
 
237
+ async function unary(methodName, body, extra) {
238
+ return unaryForService(CATALOG_SERVICE, methodName, body, extra);
239
+ }
240
+
236
241
  return {
237
242
  searchAssets: function searchAssets(body, extra) {
238
243
  return unary("SearchAssets", body, extra);
239
244
  },
245
+ searchUsers: function searchUsers(body, extra) {
246
+ return unaryForService(IDENTITY_SERVICE, "SearchUsers", body, extra);
247
+ },
240
248
  listUserAssets: function listUserAssets(body, extra) {
241
249
  return unary("ListUserAssets", body, extra);
242
250
  },
@@ -19,6 +19,9 @@ const COMMAND_LOADERS = {
19
19
  search: function loadSearch() {
20
20
  return require("./search");
21
21
  },
22
+ users: function loadUsers() {
23
+ return require("./users");
24
+ },
22
25
  info: function loadInfo() {
23
26
  return require("./info");
24
27
  },
@@ -0,0 +1,247 @@
1
+ "use strict";
2
+
3
+ const { parseOptions } = require("../cli-parser");
4
+ const { createCatalogClient } = require("../api");
5
+ const { CLI_NAME } = require("../cli-text");
6
+ const { resolveApiBaseUrl, resolveConnectBaseUrl } = require("../config");
7
+ const { generateRequestId } = require("../utils");
8
+
9
+ const DEFAULT_PAGE_SIZE = 10;
10
+ const DEFAULT_SORT_BY = "SEARCH_USERS_SORT_BY_BEST_MATCH";
11
+
12
+ function printHelp() {
13
+ console.log(CLI_NAME + " users");
14
+ console.log("");
15
+ console.log("用法:");
16
+ console.log(" " + CLI_NAME + " users <keyword> [options]");
17
+ console.log("");
18
+ console.log("参数:");
19
+ console.log(" --page-size <number> 每页条数,默认 10");
20
+ console.log(" --page-token <token> 分页游标");
21
+ console.log(" --request-id <id> 手动指定 X-Request-ID");
22
+ console.log(" --verbose 输出更多上下文");
23
+ console.log(" -h, --help 查看帮助");
24
+ console.log("");
25
+ console.log("示例:");
26
+ console.log(" " + CLI_NAME + " users openclawmp.cc");
27
+ console.log(" " + CLI_NAME + " users 窦窦 --page-size 20");
28
+ }
29
+
30
+ function pickFirstText(values) {
31
+ return values.find(function findValue(value) {
32
+ return typeof value === "string" && value.trim();
33
+ }) || "";
34
+ }
35
+
36
+ function pickFirstScalar(values) {
37
+ return values.find(function findValue(value) {
38
+ if (typeof value === "string") {
39
+ return value.trim();
40
+ }
41
+ return typeof value === "number" && Number.isFinite(value);
42
+ });
43
+ }
44
+
45
+ function normalizePageSize(value) {
46
+ if (value === undefined) {
47
+ return DEFAULT_PAGE_SIZE;
48
+ }
49
+
50
+ if (!Number.isInteger(value) || value <= 0) {
51
+ throw new Error("--page-size 需要是大于 0 的整数");
52
+ }
53
+
54
+ return value;
55
+ }
56
+
57
+ function formatCount(value) {
58
+ const numeric = Number(value);
59
+ if (Number.isFinite(numeric)) {
60
+ return numeric.toLocaleString("zh-CN");
61
+ }
62
+ return String(value || "0");
63
+ }
64
+
65
+ function resolveItems(data) {
66
+ if (Array.isArray(data.items)) {
67
+ return data.items;
68
+ }
69
+ if (Array.isArray(data.users)) {
70
+ return data.users;
71
+ }
72
+ return [];
73
+ }
74
+
75
+ function resolveTotalSize(data, items) {
76
+ const total = pickFirstScalar([
77
+ data.totalSize,
78
+ data.total_size,
79
+ data.totalCount,
80
+ data.total_count,
81
+ data.total,
82
+ data.count
83
+ ]);
84
+ if (total !== undefined) {
85
+ return String(total);
86
+ }
87
+ return String(items.length);
88
+ }
89
+
90
+ function resolveNextPageToken(data) {
91
+ return pickFirstText([data.nextPageToken, data.next_page_token]);
92
+ }
93
+
94
+ function resolveUser(item) {
95
+ if (!item || typeof item !== "object") {
96
+ return {};
97
+ }
98
+ if (item.user && typeof item.user === "object") {
99
+ return item.user;
100
+ }
101
+ if (item.profile && typeof item.profile === "object") {
102
+ return item.profile;
103
+ }
104
+ return item;
105
+ }
106
+
107
+ function resolveDisplayName(user) {
108
+ return pickFirstText([
109
+ user.displayName,
110
+ user.name,
111
+ user.nickname,
112
+ user.userId
113
+ ]) || "(未命名用户)";
114
+ }
115
+
116
+ function formatDescription(user) {
117
+ const raw = pickFirstText([
118
+ user.introduction,
119
+ user.description,
120
+ user.bio,
121
+ user.summary
122
+ ]);
123
+ const singleLine = raw.replace(/\s+/g, " ").trim();
124
+ if (!singleLine) {
125
+ return "(无描述)";
126
+ }
127
+ if (singleLine.length <= 120) {
128
+ return singleLine;
129
+ }
130
+ return singleLine.slice(0, 117) + "...";
131
+ }
132
+
133
+ function buildHomepageUrl(user, siteBaseUrl) {
134
+ const explicitUrl = pickFirstText([
135
+ user.homepageUrl,
136
+ user.homePageUrl,
137
+ user.profileUrl,
138
+ user.publicProfileUrl,
139
+ user.url,
140
+ user.link
141
+ ]);
142
+ if (explicitUrl) {
143
+ return String(new URL(explicitUrl, String(siteBaseUrl).replace(/\/+$/u, "") + "/"));
144
+ }
145
+
146
+ const userId = pickFirstText([user.userId, user.uid, user.id]);
147
+ if (!userId) {
148
+ return "(无主页链接)";
149
+ }
150
+
151
+ return String(siteBaseUrl).replace(/\/+$/u, "") + "/user/" + encodeURIComponent(userId);
152
+ }
153
+
154
+ function printUserSearchResult(item, index, siteBaseUrl) {
155
+ const user = resolveUser(item);
156
+
157
+ console.log(index + ". " + resolveDisplayName(user));
158
+ console.log(" 描述: " + formatDescription(user));
159
+ console.log(" 主页: " + buildHomepageUrl(user, siteBaseUrl));
160
+ }
161
+
162
+ async function run(context) {
163
+ const parsed = parseOptions(context.argv, {
164
+ "page-size": { type: "number" },
165
+ "page-token": { type: "string" },
166
+ "request-id": { type: "string" },
167
+ verbose: { type: "boolean" }
168
+ });
169
+
170
+ const query = parsed.positionals.join(" ").trim();
171
+ if (!query) {
172
+ throw new Error("users 命令缺少关键词");
173
+ }
174
+
175
+ const pageSize = normalizePageSize(parsed.options["page-size"]);
176
+ const requestId = parsed.options["request-id"] || generateRequestId();
177
+ const apiBaseUrl = resolveApiBaseUrl();
178
+ const client = createCatalogClient({
179
+ baseUrl: resolveConnectBaseUrl(),
180
+ cliVersion: context.cliVersion,
181
+ requestId,
182
+ verbose: Boolean(parsed.options.verbose)
183
+ });
184
+
185
+ const response = await client.searchUsers({
186
+ query,
187
+ sortBy: DEFAULT_SORT_BY,
188
+ pageSize,
189
+ pageToken: parsed.options["page-token"] || ""
190
+ });
191
+
192
+ const items = resolveItems(response.data);
193
+ const totalSize = resolveTotalSize(response.data, items);
194
+ const nextPageToken = resolveNextPageToken(response.data);
195
+
196
+ if (parsed.options.verbose) {
197
+ console.log("request-id: " + response.requestId);
198
+ }
199
+
200
+ if (items.length === 0) {
201
+ if (Number(totalSize) > 0) {
202
+ console.log("当前页没有可展示的用户结果。");
203
+ } else {
204
+ console.log("没有找到与 \"" + query + "\" 相关的用户。");
205
+ }
206
+ console.log("总结果数: " + formatCount(totalSize));
207
+
208
+ return {
209
+ items: [],
210
+ nextPageToken,
211
+ totalSize
212
+ };
213
+ }
214
+
215
+ console.log("用户搜索结果:共 " + formatCount(totalSize) + " 项,本页 " + items.length + " 项");
216
+ console.log("");
217
+
218
+ items.forEach(function eachItem(item, index) {
219
+ printUserSearchResult(item, index + 1, apiBaseUrl);
220
+ if (index < items.length - 1) {
221
+ console.log("");
222
+ }
223
+ });
224
+
225
+ if (nextPageToken) {
226
+ let nextPageCommand = " " + CLI_NAME + " users " + JSON.stringify(query);
227
+ if (pageSize !== DEFAULT_PAGE_SIZE) {
228
+ nextPageCommand += " --page-size " + pageSize;
229
+ }
230
+ nextPageCommand += " --page-token " + nextPageToken;
231
+
232
+ console.log("");
233
+ console.log("下一页:");
234
+ console.log(nextPageCommand);
235
+ }
236
+
237
+ return {
238
+ items,
239
+ nextPageToken,
240
+ totalSize
241
+ };
242
+ }
243
+
244
+ module.exports = {
245
+ printHelp,
246
+ run
247
+ };
package/lib/help.js CHANGED
@@ -28,6 +28,11 @@ const COMMAND_SECTIONS = [
28
28
  description: "搜索市场资产",
29
29
  usage: CLI_NAME + " search <query> [options]"
30
30
  },
31
+ {
32
+ name: "users",
33
+ description: "搜索用户",
34
+ usage: CLI_NAME + " users <keyword> [options]"
35
+ },
31
36
  {
32
37
  name: "info",
33
38
  description: "查看某个资产的详细信息",
@@ -76,6 +81,7 @@ function printGlobalHelp() {
76
81
  console.log(" " + CLI_NAME + " oauth <your-token>");
77
82
  console.log(" " + CLI_NAME + " publish ./my-asset --yes");
78
83
  console.log(" " + CLI_NAME + " search 天气");
84
+ console.log(" " + CLI_NAME + " users openclawmp.cc");
79
85
  console.log(" " + CLI_NAME + " info 6c476a36955a4270a3fa303beeeed5ee");
80
86
  console.log(" " + buildInstallCommand("skill", "7c19dc4c3244418096f1dcb59c93f795"));
81
87
  console.log(" " + buildInstallCommand("skill", "7c19dc4c3244418096f1dcb59c93f795", "1.0.3"));
package/lib/utils.js CHANGED
@@ -197,6 +197,8 @@ function humanizeApiMethodName(methodName) {
197
197
  return "取消上传任务";
198
198
  case "SearchAssets":
199
199
  return "搜索资产";
200
+ case "SearchUsers":
201
+ return "搜索用户";
200
202
  case "ListUserAssets":
201
203
  return "查询用户资产";
202
204
  case "GetAsset":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclawmp",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "OpenClaw Marketplace CLI client",
5
5
  "bin": {
6
6
  "openclawmp": "bin/openclawmp.js"
@@ -15,7 +15,8 @@
15
15
  },
16
16
  "scripts": {
17
17
  "start": "node bin/openclawmp.js",
18
- "publish:dry-run": "node bin/openclawmp.js publish . --dry-run"
18
+ "publish:dry-run": "node bin/openclawmp.js publish . --dry-run",
19
+ "pack:local": "npm pack"
19
20
  },
20
21
  "directories": {
21
22
  "lib": "lib",