Compare commits

...

12 Commits

Author SHA1 Message Date
ywkj 9fe8ab94e8 feat: auth-rt 改用 Go 二进制,install.sh 自动下载
register-skill-release / register (push) Successful in 13s Details
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 20:52:02 +08:00
ywkj f4fe52aff1 fix: auth-rt auto-install 使用永久路径 ~/.local/share/auth-runtime
修复之前 clone 到 /tmp 后删除导致 wrapper 指向不存在路径的问题。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 20:34:21 +08:00
ywkj b29037526b feat: install.sh 自动下载安装 auth-rt(无需手动 clone)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 18:36:47 +08:00
ywkj ece7db4cf2 chore: auth-rt 默认路径改为 ~/.local/bin/
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 18:35:37 +08:00
ywkj 2f4f8d4030 fix: auth-cli.ts 修复 HOME 路径解析 + shell wrapper 兼容
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 18:31:06 +08:00
ywkj 47d3711f43 refactor: auth-cli.ts 改用 auth-rt 二进制
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 18:26:29 +08:00
ywkj 2beb7e1051 refactor: 移除 @clawd/auth-runtime npm 依赖,改用 CLI subprocess 调用
通过内置 auth-cli.ts 薄 wrapper 调用 auth-runtime CLI,
消除 npm git 依赖带来的缓存和更新问题。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 14:38:05 +08:00
ywkj dfbd601307 fix: add install.sh to always fetch latest git deps
register-skill-release / register (push) Successful in 14s Details
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 06:56:31 +08:00
ywkj f7d6cb125c chore: pin @clawd/auth-runtime to v1.1.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 06:49:08 +08:00
ywkj 3bf26335a6 chore: clean CLIENT_KEY references from SKILL.md and .env.example
Auth is handled automatically by auth-runtime — no need to expose
CLIENT_KEY in skill-level documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 08:31:57 +08:00
ywkj 06c5c302e9 refactor: simplify index.ts — use ctx object instead of 8-param helpers
Replace createFailedResult/createSuccessResult (8+ params each) with
a single result() builder + ctx spread. Default country to 'US'.
-117 lines, same output_schema.json contract.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 07:16:46 +08:00
ywkj e7d9e972d4 refactor: use SkillClient, remove manual auth handling
Replace createEnvConfig/getAccessToken/requestApiWithAutoRefresh
with createSkillClient(). workflow.ts now takes a SkillClient
instead of config+token. Remove EnvConfig extension from types.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 08:25:09 +08:00
10 changed files with 201 additions and 236 deletions

View File

@ -1,6 +1,4 @@
# Local runtime config for client-finder skill # Local runtime config for client-finder skill
# Copy to .env.local in the same folder and fill real values. # Auth is handled automatically via ~/.openclaw/.env
AUTH_BASE=https://api-gw-test.yuanwei-lnc.com AUTH_BASE=https://api-gw-test.yuanwei-lnc.com
CLIENT_KEY=sk_xxx_replace_with_real_key

View File

@ -8,7 +8,7 @@ description: "找客户、找买家、开发客户、cold outreach。当用户
Skill-credit + ecom run-flow: query expansion → `/ecom/cold-outreach/run-flow`. Skill-credit + ecom run-flow: query expansion → `/ecom/cold-outreach/run-flow`.
Execution mode is fire-and-return: start workflow fast and return accepted immediately; terminal callbacks are handled by backend webhook delivery. Execution mode is fire-and-return: start workflow fast and return accepted immediately; terminal callbacks are handled by backend webhook delivery.
> Auth (CLIENT_KEY) is loaded automatically from `~/.openclaw/.env` by auth-runtime. No need to pass it. > Auth is handled automatically by auth-runtime via `~/.openclaw/.env`.
## Run Skill ## Run Skill
@ -51,8 +51,7 @@ For client onboarding and billing flow (Chinese), read [how-to-use.md](how-to-us
- Remove leading `cold-outreach:` prefix (case-insensitive). - Remove leading `cold-outreach:` prefix (case-insensitive).
2. Exchange runtime token. 2. Exchange runtime token.
- auth-runtime handles this automatically via `~/.openclaw/.env`. - Auth handled automatically by auth-runtime.
- Calls `POST /auth/skill-credit/session` with `CLIENT_KEY`, caches token with TTL.
3. Expand query. 3. Expand query.
- Build candidate queries from skill logic (`rule`) or `QUERY_EXPANSION_JSON` (`llm`). - Build candidate queries from skill logic (`rule`) or `QUERY_EXPANSION_JSON` (`llm`).

View File

@ -5,7 +5,7 @@
"": { "": {
"name": "client-finder", "name": "client-finder",
"dependencies": { "dependencies": {
"@clawd/auth-runtime": "file:../_shared/auth-runtime", "@clawd/auth-runtime": "file:../auth-runtime",
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",
@ -17,11 +17,11 @@
}, },
}, },
"packages": { "packages": {
"@clawd/auth-runtime": ["@clawd/auth-runtime@file:../_shared/auth-runtime", { "devDependencies": { "@types/node": "^25.3.3", "typescript": "^5.9.3" } }], "@clawd/auth-runtime": ["@clawd/auth-runtime@file:../auth-runtime", { "devDependencies": { "@types/node": "^25.3.3", "typescript": "^5.9.3" } }],
"@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
"@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="], "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
"bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],

26
install.sh Executable file
View File

@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
# Auto-install auth-rt if not found
if ! command -v auth-rt &>/dev/null && [ ! -x "$HOME/.local/bin/auth-rt" ]; then
echo "auth-rt not found, installing..."
_FORGEJO="http://192.168.0.108:3030"
_OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
_ARCH="$(uname -m)"; case "$_ARCH" in x86_64) _ARCH="amd64";; aarch64) _ARCH="arm64";; esac
_URL="$_FORGEJO/agent-skills/auth-runtime/releases/download/latest/auth-rt-${_OS}-${_ARCH}"
mkdir -p "$HOME/.local/bin"
if curl -fsSL "$_URL" -o "$HOME/.local/bin/auth-rt" 2>/dev/null; then
chmod +x "$HOME/.local/bin/auth-rt"
echo "auth-rt installed (downloaded)"
else
echo "Download failed, building from source..."
_SRC="$HOME/.local/share/auth-runtime"
if [ -d "$_SRC/.git" ]; then git -C "$_SRC" pull --ff-only
else git clone --depth 1 "$_FORGEJO/agent-skills/auth-runtime.git" "$_SRC"
fi
bash "$_SRC/install.sh"
fi
fi
npm install

View File

@ -18,7 +18,5 @@
"peerDependencies": { "peerDependencies": {
"typescript": "^5.0.0" "typescript": "^5.0.0"
}, },
"dependencies": { "dependencies": {}
"@clawd/auth-runtime": "git+http://192.168.0.108:3030/agent-skills/auth-runtime.git"
}
} }

View File

@ -87,7 +87,7 @@ async function main(): Promise<void> {
if (parsed.clientKey) process.env.CLIENT_KEY = parsed.clientKey; if (parsed.clientKey) process.env.CLIENT_KEY = parsed.clientKey;
if (parsed.authBase) process.env.AUTH_BASE = parsed.authBase; if (parsed.authBase) process.env.AUTH_BASE = parsed.authBase;
const result = await runClientFinder(parsed.query, parsed.country, parsed.dryRun); const result = await runClientFinder(parsed.query, parsed.country || 'US', parsed.dryRun);
console.log(JSON.stringify(result, null, 2)); console.log(JSON.stringify(result, null, 2));
} }

119
src/auth-cli.ts Normal file
View File

@ -0,0 +1,119 @@
/**
* Thin CLI wrapper for auth-runtime.
*
* Copy this file into your skill's src/ directory. It calls the
* `auth-rt` binary (a standalone Go executable), so the skill has
* zero npm/runtime dependency on auth-runtime.
*
* Prerequisites:
* `auth-rt` must be in PATH or at ~/.local/bin/auth-rt
* (install.sh handles this automatically)
*
* Usage:
* import { createSkillClient } from './auth-cli.ts';
* const client = createSkillClient();
* const res = await client.post('/ecom/tasks/scrape', { url: '...' });
*/
import { spawnSync } from 'child_process';
import * as path from 'path';
import * as os from 'os';
const home = process.env.HOME || os.homedir();
const AUTH_RT_BIN = process.env.AUTH_RT_BIN
|| (() => {
// Check if auth-rt is in PATH
const which = spawnSync('which', ['auth-rt'], { encoding: 'utf-8' });
if (which.status === 0 && which.stdout.trim()) {
return which.stdout.trim();
}
return path.join(home, '.local', 'bin', 'auth-rt');
})();
export interface ApiResponse {
status: number;
body: string;
}
export interface SessionResponse {
accessToken: string;
expiresIn: number;
ownerSessionToken?: string;
hookUrl?: string;
hookToken?: string;
}
export interface SkillClientOptions {
apiBase?: string;
dryRun?: boolean;
}
function runCli(...args: string[]): string {
const result = spawnSync(AUTH_RT_BIN, args, {
encoding: 'utf-8',
timeout: 60_000,
});
if (result.error) {
throw new Error(`auth-rt spawn failed: ${result.error.message}`);
}
if (result.status !== 0) {
throw new Error(`auth-rt failed (exit ${result.status}): ${(result.stderr || '').trim()}`);
}
return (result.stdout || '').trim();
}
export class SkillClient {
private readonly apiBase?: string;
private readonly dryRun: boolean;
constructor(options: SkillClientOptions = {}) {
this.apiBase = options.apiBase;
this.dryRun = options.dryRun ?? false;
}
async session(): Promise<SessionResponse> {
if (this.dryRun) {
return { accessToken: '<dry-run-token>', expiresIn: 900 };
}
return JSON.parse(runCli('session'));
}
async get(urlPath: string): Promise<ApiResponse> {
return this.request('GET', urlPath);
}
async post(urlPath: string, body?: unknown): Promise<ApiResponse> {
return this.request('POST', urlPath, body);
}
async put(urlPath: string, body?: unknown): Promise<ApiResponse> {
return this.request('PUT', urlPath, body);
}
async patch(urlPath: string, body?: unknown): Promise<ApiResponse> {
return this.request('PATCH', urlPath, body);
}
async delete(urlPath: string, body?: unknown): Promise<ApiResponse> {
return this.request('DELETE', urlPath, body);
}
private async request(method: string, urlPath: string, body?: unknown): Promise<ApiResponse> {
if (this.dryRun) {
return { status: 200, body: JSON.stringify({ dryRun: true, method, path: urlPath }) };
}
const args = ['request', method, urlPath];
if (body != null) {
args.push('--body', JSON.stringify(body));
}
if (this.apiBase) {
args.push('--api-base', this.apiBase);
}
return JSON.parse(runCli(...args));
}
}
export function createSkillClient(options?: SkillClientOptions): SkillClient {
return new SkillClient(options);
}

View File

@ -1,199 +1,80 @@
import type { EnvConfig, OutputResult } from './types.js'; import type { OutputResult } from './types.js';
import { createEnvConfig as createBaseEnvConfig, getAccessToken } from '@clawd/auth-runtime'; import { createSkillClient } from './auth-cli.ts';
import { normalizeQuery, resolveExpansion } from './expansion.js'; import { normalizeQuery, resolveExpansion } from './expansion.js';
import { startWorkflow } from './workflow.js'; import { startWorkflow } from './workflow.js';
/**
* Create client-finder specific environment configuration
* Extends the shared auth config with skill-specific fields
*/
function createEnvConfig(): EnvConfig {
const baseConfig = createBaseEnvConfig();
return {
...baseConfig,
queryExpansionJson: process.env.QUERY_EXPANSION_JSON || '',
};
}
/**
* Main entry point for client-finder skill
*/
export async function runClientFinder( export async function runClientFinder(
query: string, query: string,
country: string, country: string = 'US',
dryRun: boolean = false, dryRun: boolean = false,
): Promise<OutputResult> { ): Promise<OutputResult> {
const config = createEnvConfig();
// Validate query
if (!query) { if (!query) {
return createFailedResult('', 'missing query argument'); return result('failed', { error: 'missing query argument' });
} }
// Normalize query
const rawQuery = normalizeQuery(query); const rawQuery = normalizeQuery(query);
const countryUpper = country.toUpperCase(); const countryUpper = country.toUpperCase();
const countryLower = country.toLowerCase(); const llmExpansion = process.env.QUERY_EXPANSION_JSON || '';
const expansion = resolveExpansion(rawQuery, countryUpper, llmExpansion);
// Resolve expansion // Build expansion context
const expansion = resolveExpansion( const ctx: Partial<OutputResult> = {
rawQuery, inputQuery: rawQuery,
countryUpper, expandedQueries: expansion.expandedQueries,
config.queryExpansionJson, primaryQuery: expansion.primaryQuery,
); expansionStatus: expansion.ok ? 'success' : 'failed',
expansionSource: expansion.expansionSource,
expansionError: expansion.error || null,
usedFallbackQuery: false,
};
let expandedQueries = expansion.expandedQueries; // Handle expansion failure — fallback to raw query
let primaryQuery = expansion.primaryQuery;
let expansionStatus = expansion.ok ? 'success' : ('failed' as const);
let expansionSource = expansion.expansionSource;
let expansionError = expansion.error || '';
let usedFallbackQuery = false;
// Handle expansion failure
if (!expansion.ok) { if (!expansion.ok) {
if (rawQuery) { if (!rawQuery) {
primaryQuery = rawQuery; return result('failed', { ...ctx, error: expansion.error || 'query is empty' });
expandedQueries = [rawQuery];
expansionSource = 'raw_query';
expansionError = `query expansion failed: ${expansion.error}; fallback to raw query`;
usedFallbackQuery = true;
} else {
expansionError = expansionError || 'query expansion failed: primary query is empty';
return createFailedResult(
rawQuery,
expansionError,
expandedQueries,
primaryQuery,
expansionStatus,
expansionSource,
expansionError,
usedFallbackQuery,
);
} }
ctx.primaryQuery = rawQuery;
ctx.expandedQueries = [rawQuery];
ctx.expansionSource = 'raw_query';
ctx.expansionError = `query expansion failed: ${expansion.error}; fallback to raw query`;
ctx.usedFallbackQuery = true;
} }
// Validate primary query if (!ctx.primaryQuery) {
if (!primaryQuery) { return result('failed', { ...ctx, error: 'primary query is empty after expansion' });
expansionError = expansionError || 'query expansion failed: primary query is empty';
return createFailedResult(
rawQuery,
expansionError,
expandedQueries,
primaryQuery,
expansionStatus,
expansionSource,
expansionError,
usedFallbackQuery,
);
} }
// Dry run mode
if (dryRun) { if (dryRun) {
return createSuccessResult( return result('success', { ...ctx, workflowStatus: 'dry_run' });
rawQuery,
expandedQueries,
primaryQuery,
expansionStatus,
expansionSource,
expansionError,
usedFallbackQuery,
'',
'dry_run',
);
} }
// Validate CLIENT_KEY in live mode let client;
if (!config.clientKey) {
return createFailedResult(
rawQuery,
'missing required env: CLIENT_KEY',
expandedQueries,
primaryQuery,
expansionStatus,
expansionSource,
expansionError,
usedFallbackQuery,
);
}
// Step 1: Exchange CLIENT_KEY for runtime access token
let accessToken = '';
try { try {
accessToken = await getAccessToken(dryRun, config); client = createSkillClient();
} catch (error) { } catch (error) {
const errorMsg = error instanceof Error ? error.message : 'failed to exchange skill session token'; return result('failed', { ...ctx, error: error instanceof Error ? error.message : String(error) });
return createFailedResult(
rawQuery,
errorMsg,
expandedQueries,
primaryQuery,
expansionStatus,
expansionSource,
expansionError,
usedFallbackQuery,
);
} }
// Step 2: Start workflow with runtime access token const wf = await startWorkflow(client, ctx.primaryQuery, country.toLowerCase());
const workflowResult = await startWorkflow(
config,
dryRun,
accessToken,
primaryQuery,
countryLower,
);
if (!workflowResult.workflowId) { if (!wf.workflowId) {
return createFailedResult( return result('failed', { ...ctx, error: `start failed: ${wf.error}` });
rawQuery,
`start failed: ${workflowResult.error}`,
expandedQueries,
primaryQuery,
expansionStatus,
expansionSource,
expansionError,
usedFallbackQuery,
);
} }
// Step 3: Return accepted immediately return result('success', { ...ctx, workflowId: wf.workflowId, workflowStatus: 'accepted' });
return createSuccessResult(
rawQuery,
expandedQueries,
primaryQuery,
expansionStatus,
expansionSource,
expansionError,
usedFallbackQuery,
'',
'accepted',
workflowResult.workflowId,
);
} }
/** function result(status: 'success' | 'failed', overrides: Partial<OutputResult> = {}): OutputResult {
* Create a failed result
*/
function createFailedResult(
inputQuery: string,
error: string,
expandedQueries: string[] = [],
primaryQuery: string = '',
expansionStatus: 'success' | 'failed' = 'failed',
expansionSource: 'llm' | 'rule' | 'raw_query' | '' = '',
expansionError: string | null = null,
usedFallbackQuery: boolean = false,
): OutputResult {
return { return {
status: 'failed', status,
error: error || null, error: null,
inputQuery, inputQuery: '',
expandedQueries, expandedQueries: [],
primaryQuery, primaryQuery: '',
expansionStatus, expansionStatus: 'failed',
expansionSource, expansionSource: '',
expansionError: expansionError || null, expansionError: null,
usedFallbackQuery, usedFallbackQuery: false,
runId: '', runId: '',
workflowId: '', workflowId: '',
workflowStatus: '', workflowStatus: '',
@ -202,41 +83,6 @@ function createFailedResult(
businessesCount: 0, businessesCount: 0,
contactsCount: 0, contactsCount: 0,
uniqueContactDomains: 0, uniqueContactDomains: 0,
}; ...overrides,
}
/**
* Create a success result
*/
function createSuccessResult(
inputQuery: string,
expandedQueries: string[],
primaryQuery: string,
expansionStatus: 'success' | 'failed',
expansionSource: 'llm' | 'rule' | 'raw_query' | '',
expansionError: string | null,
usedFallbackQuery: boolean,
runId: string,
workflowStatus: string,
workflowId: string = '',
): OutputResult {
return {
status: 'success',
error: null,
inputQuery,
expandedQueries,
primaryQuery,
expansionStatus,
expansionSource,
expansionError,
usedFallbackQuery,
runId,
workflowId,
workflowStatus,
billingReserveStatus: 'SKIPPED',
billingFinalizeStatus: 'SKIPPED',
businessesCount: 0,
contactsCount: 0,
uniqueContactDomains: 0,
}; };
} }

View File

@ -1,5 +1,3 @@
import type { EnvConfig as BaseEnvConfig } from '@clawd/auth-runtime';
/** /**
* Query expansion result from LLM or rule-based logic * Query expansion result from LLM or rule-based logic
*/ */
@ -45,10 +43,3 @@ export interface OutputResult {
uniqueContactDomains: number; uniqueContactDomains: number;
} }
/**
* Client-finder specific environment configuration
* Extends the shared auth config with skill-specific fields
*/
export interface EnvConfig extends BaseEnvConfig {
queryExpansionJson: string;
}

View File

@ -1,5 +1,4 @@
import { requestApiWithAutoRefresh } from '@clawd/auth-runtime'; import type { SkillClient } from './auth-cli.ts';
import type { EnvConfig as AuthEnvConfig } from '@clawd/auth-runtime';
import { WorkflowStartResponse } from './types.js'; import { WorkflowStartResponse } from './types.js';
/** /**
@ -18,26 +17,15 @@ export function parseWorkflowId(responseBody: string): string {
* Start cold outreach workflow * Start cold outreach workflow
*/ */
export async function startWorkflow( export async function startWorkflow(
config: AuthEnvConfig, client: SkillClient,
dryRun: boolean,
accessToken: string,
query: string, query: string,
country: string, country: string,
): Promise<{ workflowId: string; error: string }> { ): Promise<{ workflowId: string; error: string }> {
const payload = JSON.stringify({ const result = await client.post('/ecom/cold-outreach/run-flow', {
query, query,
country, country,
}); });
const result = await requestApiWithAutoRefresh(
'POST',
`${config.authBase}/ecom/cold-outreach/run-flow`,
dryRun,
config,
payload,
accessToken,
);
if (result.status < 200 || result.status >= 300) { if (result.status < 200 || result.status >= 300) {
const error = parseError(result.body); const error = parseError(result.body);
return { return {