@2en/clawly-plugins 1.30.0-beta.9 → 1.31.0-beta.0
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/config-setup.ts +79 -129
- package/gateway/config-model.ts +1 -7
- package/gateway/config-timezone.ts +1 -7
- package/model-gateway-setup.ts +0 -46
- package/package.json +1 -1
- package/tools/clawly-search.test.ts +48 -1
- package/tools/clawly-search.ts +14 -0
- package/tools/create-search-tool.ts +27 -4
- package/tools/index.ts +7 -1
package/config-setup.ts
CHANGED
|
@@ -24,7 +24,6 @@ import {
|
|
|
24
24
|
ENV_KEY_API_KEY,
|
|
25
25
|
ENV_KEY_BASE,
|
|
26
26
|
PROVIDER_NAME,
|
|
27
|
-
patchModelGateway,
|
|
28
27
|
readOpenclawConfig,
|
|
29
28
|
stripPathname,
|
|
30
29
|
writeOpenclawConfig,
|
|
@@ -340,48 +339,82 @@ export function patchSession(config: OpenClawConfig): boolean {
|
|
|
340
339
|
return dirty
|
|
341
340
|
}
|
|
342
341
|
|
|
343
|
-
const
|
|
342
|
+
const DEFAULTS_PATH = './extensions/clawly-plugins/clawly-config-defaults.json5'
|
|
343
|
+
const LEGACY_INCLUDE_PATH = DEFAULTS_PATH
|
|
344
344
|
|
|
345
345
|
/**
|
|
346
|
-
*
|
|
347
|
-
* Returns true if
|
|
346
|
+
* Remove legacy `$include` reference to our defaults file.
|
|
347
|
+
* Returns true if config was modified.
|
|
348
348
|
*/
|
|
349
|
-
export function
|
|
349
|
+
export function removeInclude(config: OpenClawConfig & Record<string, unknown>): boolean {
|
|
350
350
|
const existing = config.$include
|
|
351
|
-
|
|
351
|
+
if (existing === undefined) return false
|
|
352
352
|
|
|
353
|
-
if (
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
353
|
+
if (typeof existing === 'string') {
|
|
354
|
+
if (existing === LEGACY_INCLUDE_PATH) {
|
|
355
|
+
delete config.$include
|
|
356
|
+
return true
|
|
357
|
+
}
|
|
358
|
+
return false
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
-
if (
|
|
361
|
+
if (Array.isArray(existing)) {
|
|
362
|
+
const includes = existing as string[]
|
|
363
|
+
const idx = includes.indexOf(LEGACY_INCLUDE_PATH)
|
|
364
|
+
if (idx === -1) return false
|
|
365
|
+
includes.splice(idx, 1)
|
|
366
|
+
if (includes.length === 0) {
|
|
367
|
+
delete config.$include
|
|
368
|
+
} else if (includes.length === 1) {
|
|
369
|
+
config.$include = includes[0]
|
|
370
|
+
}
|
|
371
|
+
return true
|
|
372
|
+
}
|
|
362
373
|
|
|
363
|
-
|
|
364
|
-
config.$include = includes.length === 1 ? includes[0] : includes
|
|
365
|
-
return true
|
|
374
|
+
return false
|
|
366
375
|
}
|
|
367
376
|
|
|
368
377
|
/**
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
378
|
+
* Deep-merge defaults into config. Config values take precedence;
|
|
379
|
+
* defaults only fill in missing keys. Arrays are not merged — config wins.
|
|
380
|
+
* Returns true if any key was added.
|
|
372
381
|
*/
|
|
373
|
-
function
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
382
|
+
export function deepMergeDefaults(
|
|
383
|
+
config: Record<string, unknown>,
|
|
384
|
+
defaults: Record<string, unknown>,
|
|
385
|
+
): boolean {
|
|
386
|
+
let merged = false
|
|
387
|
+
for (const key of Object.keys(defaults)) {
|
|
388
|
+
const defaultVal = defaults[key]
|
|
389
|
+
const configVal = config[key]
|
|
390
|
+
|
|
391
|
+
if (configVal === undefined) {
|
|
392
|
+
config[key] = defaultVal
|
|
393
|
+
merged = true
|
|
394
|
+
continue
|
|
381
395
|
}
|
|
382
|
-
|
|
383
|
-
//
|
|
396
|
+
|
|
397
|
+
// Both are plain objects → recurse
|
|
398
|
+
if (
|
|
399
|
+
defaultVal !== null &&
|
|
400
|
+
typeof defaultVal === 'object' &&
|
|
401
|
+
!Array.isArray(defaultVal) &&
|
|
402
|
+
configVal !== null &&
|
|
403
|
+
typeof configVal === 'object' &&
|
|
404
|
+
!Array.isArray(configVal)
|
|
405
|
+
) {
|
|
406
|
+
if (
|
|
407
|
+
deepMergeDefaults(
|
|
408
|
+
configVal as Record<string, unknown>,
|
|
409
|
+
defaultVal as Record<string, unknown>,
|
|
410
|
+
)
|
|
411
|
+
) {
|
|
412
|
+
merged = true
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// For arrays and primitives: config wins, do nothing
|
|
384
416
|
}
|
|
417
|
+
return merged
|
|
385
418
|
}
|
|
386
419
|
|
|
387
420
|
const PLUGIN_ID = 'clawly-plugins'
|
|
@@ -450,89 +483,12 @@ function repairLegacyProvisionState(api: PluginApi, config: OpenClawConfig, stat
|
|
|
450
483
|
return installRecordPatched
|
|
451
484
|
}
|
|
452
485
|
|
|
453
|
-
/**
|
|
454
|
-
* Remove fields from config that are identical to the included defaults.
|
|
455
|
-
* Since `$include` deep-merges with main config winning, residual values
|
|
456
|
-
* from old JS set-if-missing code "shadow" the json5 defaults and prevent
|
|
457
|
-
* future default updates from taking effect.
|
|
458
|
-
*
|
|
459
|
-
* Only prunes leaf values that exactly match.
|
|
460
|
-
* Returns true if any field was deleted.
|
|
461
|
-
*/
|
|
462
|
-
function stableStringify(v: unknown): string {
|
|
463
|
-
if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
|
|
464
|
-
const obj = v as Record<string, unknown>
|
|
465
|
-
return `{${Object.keys(obj)
|
|
466
|
-
.sort()
|
|
467
|
-
.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`)
|
|
468
|
-
.join(',')}}`
|
|
469
|
-
}
|
|
470
|
-
return JSON.stringify(v)
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
export function pruneIncludedDefaults(
|
|
474
|
-
config: Record<string, unknown>,
|
|
475
|
-
defaults: Record<string, unknown>,
|
|
476
|
-
): boolean {
|
|
477
|
-
let pruned = false
|
|
478
|
-
|
|
479
|
-
function walk(defaultsNode: Record<string, unknown>, configNode: Record<string, unknown>): void {
|
|
480
|
-
for (const key of Object.keys(defaultsNode)) {
|
|
481
|
-
const defaultVal = defaultsNode[key]
|
|
482
|
-
const configVal = configNode[key]
|
|
483
|
-
if (configVal === undefined) continue
|
|
484
|
-
|
|
485
|
-
// Both are plain objects → recurse
|
|
486
|
-
if (
|
|
487
|
-
defaultVal !== null &&
|
|
488
|
-
typeof defaultVal === 'object' &&
|
|
489
|
-
!Array.isArray(defaultVal) &&
|
|
490
|
-
configVal !== null &&
|
|
491
|
-
typeof configVal === 'object' &&
|
|
492
|
-
!Array.isArray(configVal)
|
|
493
|
-
) {
|
|
494
|
-
walk(defaultVal as Record<string, unknown>, configVal as Record<string, unknown>)
|
|
495
|
-
// Clean up empty parent objects left behind after pruning
|
|
496
|
-
if (Object.keys(configVal as Record<string, unknown>).length === 0) {
|
|
497
|
-
delete configNode[key]
|
|
498
|
-
pruned = true
|
|
499
|
-
}
|
|
500
|
-
continue
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// Both are non-empty arrays → remove config elements that exist in defaults
|
|
504
|
-
if (Array.isArray(defaultVal) && defaultVal.length > 0 && Array.isArray(configVal)) {
|
|
505
|
-
const defaultSet = new Set(defaultVal.map((v) => stableStringify(v)))
|
|
506
|
-
const filtered = configVal.filter((v) => !defaultSet.has(stableStringify(v)))
|
|
507
|
-
if (filtered.length !== configVal.length) {
|
|
508
|
-
if (filtered.length === 0) {
|
|
509
|
-
delete configNode[key]
|
|
510
|
-
} else {
|
|
511
|
-
configNode[key] = filtered
|
|
512
|
-
}
|
|
513
|
-
pruned = true
|
|
514
|
-
}
|
|
515
|
-
continue
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// Leaf comparison: use stableStringify for key-order-insensitive deep equality
|
|
519
|
-
if (stableStringify(configVal) === stableStringify(defaultVal)) {
|
|
520
|
-
delete configNode[key]
|
|
521
|
-
pruned = true
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
walk(defaults, config)
|
|
527
|
-
return pruned
|
|
528
|
-
}
|
|
529
|
-
|
|
530
486
|
/**
|
|
531
487
|
* Load and parse clawly-config-defaults.json5 from the plugin install directory.
|
|
532
488
|
* Uses the bundled `json5` package (declared in package.json).
|
|
533
489
|
*/
|
|
534
|
-
function
|
|
535
|
-
const defaultsPath = path.join(stateDir,
|
|
490
|
+
function loadDefaults(stateDir: string): Record<string, unknown> | null {
|
|
491
|
+
const defaultsPath = path.join(stateDir, DEFAULTS_PATH)
|
|
536
492
|
try {
|
|
537
493
|
const raw = fs.readFileSync(defaultsPath, 'utf-8')
|
|
538
494
|
return JSON5.parse(raw) as Record<string, unknown>
|
|
@@ -565,13 +521,12 @@ function reconcileRuntimeConfig(
|
|
|
565
521
|
dirty = patchGateway(config) || dirty
|
|
566
522
|
dirty = patchBrowser(config) || dirty
|
|
567
523
|
dirty = patchSession(config) || dirty
|
|
568
|
-
dirty = patchModelGateway(config, api) || dirty
|
|
569
524
|
|
|
570
|
-
const defaults =
|
|
525
|
+
const defaults = loadDefaults(stateDir)
|
|
571
526
|
if (defaults) {
|
|
572
|
-
dirty =
|
|
527
|
+
dirty = deepMergeDefaults(config, defaults) || dirty
|
|
573
528
|
} else {
|
|
574
|
-
api.logger.warn('Config setup: failed to load
|
|
529
|
+
api.logger.warn('Config setup: failed to load defaults json5, skipping merge.')
|
|
575
530
|
}
|
|
576
531
|
|
|
577
532
|
return dirty
|
|
@@ -588,22 +543,15 @@ export function setupConfig(api: PluginApi): void {
|
|
|
588
543
|
return
|
|
589
544
|
}
|
|
590
545
|
|
|
591
|
-
//
|
|
592
|
-
//
|
|
593
|
-
//
|
|
594
|
-
// runtime — resolved + SecretRef resolution, cached in-memory snapshot
|
|
595
|
-
//
|
|
596
|
-
// We need parsed: patchers mutate raw fields and write back via
|
|
597
|
-
// writeConfigFile, which diffs against the runtime snapshot and projects
|
|
598
|
-
// changes onto the source file. Using resolved/runtime would inline
|
|
599
|
-
// $include defaults into openclaw.json, defeating the json5 migration.
|
|
546
|
+
// Read the raw parsed config from openclaw.json (no env expansion).
|
|
547
|
+
// Patchers mutate fields in place; defaults from json5 are deep-merged
|
|
548
|
+
// to fill missing keys. The result is written back as the full config.
|
|
600
549
|
const configPath = path.join(stateDir, 'openclaw.json')
|
|
601
550
|
const config = readOpenclawConfig(configPath)
|
|
602
551
|
const pc = toPCMerged(api, config)
|
|
603
552
|
|
|
604
553
|
let dirty = false
|
|
605
|
-
dirty =
|
|
606
|
-
hardenIncludePermissions(stateDir, api)
|
|
554
|
+
dirty = removeInclude(config) || dirty
|
|
607
555
|
dirty = repairLegacyProvisionState(api, config, stateDir) || dirty
|
|
608
556
|
dirty = reconcileRuntimeConfig(api, config, pc, stateDir) || dirty
|
|
609
557
|
|
|
@@ -611,12 +559,14 @@ export function setupConfig(api: PluginApi): void {
|
|
|
611
559
|
try {
|
|
612
560
|
writeOpenclawConfig(configPath, config)
|
|
613
561
|
api.logger.info('Config setup: patched openclaw.json.')
|
|
614
|
-
//
|
|
615
|
-
//
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
//
|
|
619
|
-
|
|
562
|
+
// Refresh the gateway's in-memory runtime config snapshot.
|
|
563
|
+
// The sync write above updates the file on disk, but the gateway
|
|
564
|
+
// caches config via runtimeConfigSnapshot (set during startup by
|
|
565
|
+
// the secrets system). Without this refresh, loadConfig() keeps
|
|
566
|
+
// returning the stale pre-patch config until the next restart.
|
|
567
|
+
void api.runtime.config.writeConfigFile(config).catch((err) => {
|
|
568
|
+
api.logger.warn(`Config setup: runtime snapshot refresh failed: ${(err as Error).message}`)
|
|
569
|
+
})
|
|
620
570
|
} catch (err) {
|
|
621
571
|
api.logger.error(`Config setup failed: ${(err as Error).message}`)
|
|
622
572
|
}
|
package/gateway/config-model.ts
CHANGED
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
|
|
16
16
|
import type {PluginApi} from '../types'
|
|
17
17
|
import type {OpenClawConfig} from '../types/openclaw'
|
|
18
|
-
import {backfillDiskConfig} from '../model-gateway-setup'
|
|
19
18
|
|
|
20
19
|
export function registerConfigModel(api: PluginApi) {
|
|
21
20
|
api.registerGatewayMethod('clawly.config.setModel', async ({params, respond}) => {
|
|
@@ -54,13 +53,8 @@ export function registerConfigModel(api: PluginApi) {
|
|
|
54
53
|
agents.defaults = defaults
|
|
55
54
|
config.agents = agents
|
|
56
55
|
|
|
57
|
-
// Backfill fields written by setupConfig (which writes directly to disk)
|
|
58
|
-
// so writeConfigFile's merge-patch doesn't revert them.
|
|
59
|
-
const stateDir = api.runtime.state.resolveStateDir()
|
|
60
|
-
const configToWrite = stateDir ? backfillDiskConfig(stateDir, config) : config
|
|
61
|
-
|
|
62
56
|
try {
|
|
63
|
-
await api.runtime.config.writeConfigFile(
|
|
57
|
+
await api.runtime.config.writeConfigFile(config)
|
|
64
58
|
api.logger.info(`config-model: set model.primary to ${model}`)
|
|
65
59
|
respond(true, {changed: true, model})
|
|
66
60
|
} catch (err) {
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
|
|
12
12
|
import type {PluginApi} from '../types'
|
|
13
13
|
import type {OpenClawConfig} from '../types/openclaw'
|
|
14
|
-
import {backfillDiskConfig} from '../model-gateway-setup'
|
|
15
14
|
|
|
16
15
|
export function registerConfigTimezone(api: PluginApi) {
|
|
17
16
|
api.registerGatewayMethod('clawly.config.setTimezone', async ({params, respond}) => {
|
|
@@ -47,13 +46,8 @@ export function registerConfigTimezone(api: PluginApi) {
|
|
|
47
46
|
agents.defaults = defaults
|
|
48
47
|
config.agents = agents
|
|
49
48
|
|
|
50
|
-
// Backfill fields written by setupConfig (which writes directly to disk)
|
|
51
|
-
// so writeConfigFile's merge-patch doesn't revert them.
|
|
52
|
-
const stateDir = api.runtime.state.resolveStateDir()
|
|
53
|
-
const configToWrite = stateDir ? backfillDiskConfig(stateDir, config) : config
|
|
54
|
-
|
|
55
49
|
try {
|
|
56
|
-
await api.runtime.config.writeConfigFile(
|
|
50
|
+
await api.runtime.config.writeConfigFile(config)
|
|
57
51
|
api.logger.info(`config-timezone: set userTimezone to ${timezone}`)
|
|
58
52
|
respond(true, {changed: true, timezone})
|
|
59
53
|
} catch (err) {
|
package/model-gateway-setup.ts
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import fs from 'node:fs'
|
|
13
|
-
import path from 'node:path'
|
|
14
13
|
|
|
15
14
|
import type {PluginApi} from './index'
|
|
16
15
|
import type {OpenClawConfig} from './types/openclaw'
|
|
@@ -68,48 +67,3 @@ export function patchModelGateway(config: OpenClawConfig, _api: PluginApi): bool
|
|
|
68
67
|
|
|
69
68
|
return dirty
|
|
70
69
|
}
|
|
71
|
-
|
|
72
|
-
// ---------------------------------------------------------------------------
|
|
73
|
-
// backfillDiskConfig — merge setupConfig's disk writes into a loadConfig() result
|
|
74
|
-
// ---------------------------------------------------------------------------
|
|
75
|
-
|
|
76
|
-
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
77
|
-
return v !== null && typeof v === 'object' && !Array.isArray(v)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Deep-merge `source` into `target`, where source wins for any key it has.
|
|
82
|
-
* Arrays and primitives from source overwrite target.
|
|
83
|
-
* Plain objects recurse.
|
|
84
|
-
*/
|
|
85
|
-
function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): void {
|
|
86
|
-
for (const key of Object.keys(source)) {
|
|
87
|
-
if (key === '$include') continue // meta-directive, not a resolved config field
|
|
88
|
-
const sv = source[key]
|
|
89
|
-
const tv = target[key]
|
|
90
|
-
if (isPlainObject(sv) && isPlainObject(tv)) {
|
|
91
|
-
deepMerge(tv, sv)
|
|
92
|
-
} else {
|
|
93
|
-
target[key] = sv
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Read the raw disk config (openclaw.json) and deep-merge its fields into a
|
|
100
|
-
* runtime config snapshot (from `loadConfig()`). This ensures that fields
|
|
101
|
-
* written by `setupConfig` (which writes directly to disk) are present in the
|
|
102
|
-
* config object before it's passed to `writeConfigFile`.
|
|
103
|
-
*
|
|
104
|
-
* Without this, `writeConfigFile`'s inner merge-patch sees the stale runtime
|
|
105
|
-
* snapshot (missing setupConfig's changes) and reverts them on disk.
|
|
106
|
-
*/
|
|
107
|
-
export function backfillDiskConfig(stateDir: string, config: OpenClawConfig): OpenClawConfig {
|
|
108
|
-
const configPath = path.join(stateDir, 'openclaw.json')
|
|
109
|
-
const diskConfig = readOpenclawConfig(configPath)
|
|
110
|
-
// Start from disk (setupConfig's fields) then overlay config (RPC handler's
|
|
111
|
-
// changes) so the caller's modifications win over stale disk values.
|
|
112
|
-
const merged = {...diskConfig} as Record<string, unknown>
|
|
113
|
-
deepMerge(merged, config as Record<string, unknown>)
|
|
114
|
-
return merged as OpenClawConfig
|
|
115
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import {afterAll, beforeEach, describe, expect, test} from 'bun:test'
|
|
2
2
|
import type {PluginApi} from '../types'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
registerDeepSearchTool,
|
|
5
|
+
registerGrokSearchTool,
|
|
6
|
+
registerKimiSearchTool,
|
|
7
|
+
registerSearchTool,
|
|
8
|
+
} from './clawly-search'
|
|
4
9
|
|
|
5
10
|
// ── Helpers ──────────────────────────────────────────────────────
|
|
6
11
|
|
|
@@ -267,3 +272,45 @@ describe('clawly_grok_search', () => {
|
|
|
267
272
|
expect(res.error).toContain('Grok API error 429')
|
|
268
273
|
})
|
|
269
274
|
})
|
|
275
|
+
|
|
276
|
+
// ── clawly_kimi_search ──────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
describe('clawly_kimi_search', () => {
|
|
279
|
+
test('uses Kimi gateway endpoint with web_search tool', async () => {
|
|
280
|
+
mockResponse.body = {
|
|
281
|
+
choices: [{message: {content: 'Kimi answer with [source](https://example.com/page)'}}],
|
|
282
|
+
}
|
|
283
|
+
const {execute} = createMockApi(registerKimiSearchTool)
|
|
284
|
+
const res = parseResult(await execute('tc-1', {query: 'Chinese news'}))
|
|
285
|
+
|
|
286
|
+
expect(fetchCalls).toHaveLength(1)
|
|
287
|
+
const call = fetchCalls[0]
|
|
288
|
+
expect(call.url).toBe('https://gw.example.com/v1/kimi/v1/chat/completions')
|
|
289
|
+
expect(call.body?.model).toBe('kimi-k2-turbo-preview')
|
|
290
|
+
expect(call.body?.stream).toBe(false)
|
|
291
|
+
expect(call.body?.messages).toEqual([{role: 'user', content: 'Chinese news'}])
|
|
292
|
+
expect(call.body?.tools).toEqual([{type: 'builtin_function', function: {name: '$web_search'}}])
|
|
293
|
+
|
|
294
|
+
expect(res.answer).toBe('Kimi answer with [source](https://example.com/page)')
|
|
295
|
+
expect(res.citations).toEqual(['https://example.com/page'])
|
|
296
|
+
expect(res.provider).toBe('kimi')
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
test('returns empty citations when response has no inline links', async () => {
|
|
300
|
+
mockResponse.body = {choices: [{message: {content: 'Plain answer'}}]}
|
|
301
|
+
const {execute} = createMockApi(registerKimiSearchTool)
|
|
302
|
+
const res = parseResult(await execute('tc-1', {query: 'test'}))
|
|
303
|
+
|
|
304
|
+
expect(res.answer).toBe('Plain answer')
|
|
305
|
+
expect(res.citations).toEqual([])
|
|
306
|
+
expect(res.provider).toBe('kimi')
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
test('returns error on Kimi API failure', async () => {
|
|
310
|
+
mockResponse = {ok: false, status: 429, body: {error: 'rate limited'}}
|
|
311
|
+
const {execute} = createMockApi(registerKimiSearchTool)
|
|
312
|
+
const res = parseResult(await execute('tc-1', {query: 'test'}))
|
|
313
|
+
|
|
314
|
+
expect(res.error).toContain('Kimi API error 429')
|
|
315
|
+
})
|
|
316
|
+
})
|
package/tools/clawly-search.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildGrokBody,
|
|
3
3
|
buildGrokUrl,
|
|
4
|
+
buildKimiBody,
|
|
5
|
+
buildKimiUrl,
|
|
4
6
|
buildPerplexityUrl,
|
|
5
7
|
createSearchToolRegistrar,
|
|
6
8
|
parseGrokResponse,
|
|
9
|
+
parseKimiResponse,
|
|
7
10
|
} from './create-search-tool'
|
|
8
11
|
|
|
9
12
|
export const registerSearchTool = createSearchToolRegistrar({
|
|
@@ -40,3 +43,14 @@ export const registerGrokSearchTool = createSearchToolRegistrar({
|
|
|
40
43
|
buildBody: buildGrokBody,
|
|
41
44
|
parseResponse: parseGrokResponse,
|
|
42
45
|
})
|
|
46
|
+
|
|
47
|
+
export const registerKimiSearchTool = createSearchToolRegistrar({
|
|
48
|
+
toolName: 'clawly_kimi_search',
|
|
49
|
+
description: 'Search the web using Kimi. Good for Chinese-language queries and content.',
|
|
50
|
+
model: 'kimi-k2-turbo-preview',
|
|
51
|
+
buildUrl: buildKimiUrl,
|
|
52
|
+
timeoutMs: 30_000,
|
|
53
|
+
provider: 'kimi',
|
|
54
|
+
buildBody: buildKimiBody,
|
|
55
|
+
parseResponse: parseKimiResponse,
|
|
56
|
+
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type {PluginApi} from '../types'
|
|
2
2
|
import {resolveGatewayCredentials} from '../resolve-gateway-credentials'
|
|
3
3
|
|
|
4
|
-
export type SearchProvider = 'grok' | 'perplexity'
|
|
4
|
+
export type SearchProvider = 'grok' | 'kimi' | 'perplexity'
|
|
5
5
|
|
|
6
6
|
export interface SearchResult {
|
|
7
7
|
query: string
|
|
@@ -44,6 +44,19 @@ export function buildGrokBody(model: string, query: string): Record<string, unkn
|
|
|
44
44
|
return {model, input: query, tools: [{type: 'web_search'}]}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
export function buildKimiUrl(baseUrl: string): string {
|
|
48
|
+
return `${baseUrl.replace(/\/$/, '')}/kimi/v1/chat/completions`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildKimiBody(model: string, query: string): Record<string, unknown> {
|
|
52
|
+
return {
|
|
53
|
+
model,
|
|
54
|
+
stream: false,
|
|
55
|
+
messages: [{role: 'user', content: query}],
|
|
56
|
+
tools: [{type: 'builtin_function', function: {name: '$web_search'}}],
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
47
60
|
function extractCitations(text: string): string[] {
|
|
48
61
|
const urls: string[] = []
|
|
49
62
|
const seen = new Set<string>()
|
|
@@ -57,6 +70,12 @@ function extractCitations(text: string): string[] {
|
|
|
57
70
|
return urls
|
|
58
71
|
}
|
|
59
72
|
|
|
73
|
+
export function parseKimiResponse(data: unknown): {answer: string; citations: string[]} {
|
|
74
|
+
const d = data as {choices?: {message?: {content?: string}}[]}
|
|
75
|
+
const answerText = d.choices?.[0]?.message?.content ?? ''
|
|
76
|
+
return {answer: answerText, citations: extractCitations(answerText)}
|
|
77
|
+
}
|
|
78
|
+
|
|
60
79
|
export function parseGrokResponse(data: unknown): {answer: string; citations: string[]} {
|
|
61
80
|
const typed = data as {
|
|
62
81
|
output?: Array<{type: string; content?: Array<{type: string; text?: string}>}>
|
|
@@ -113,9 +132,13 @@ export function createSearchToolRegistrar(config: SearchToolConfig) {
|
|
|
113
132
|
|
|
114
133
|
if (!res.ok) {
|
|
115
134
|
const body = await res.text().catch(() => '')
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
135
|
+
const providerLabel =
|
|
136
|
+
config.provider === 'grok'
|
|
137
|
+
? 'Grok'
|
|
138
|
+
: config.provider === 'kimi'
|
|
139
|
+
? 'Kimi'
|
|
140
|
+
: 'Perplexity'
|
|
141
|
+
throw new Error(`${providerLabel} API error ${res.status}: ${body.slice(0, 200)}`)
|
|
119
142
|
}
|
|
120
143
|
|
|
121
144
|
const data = await res.json()
|
package/tools/index.ts
CHANGED
|
@@ -2,7 +2,12 @@ import type {PluginApi} from '../types'
|
|
|
2
2
|
import {registerCalendarTools} from './clawly-calendar'
|
|
3
3
|
import {registerIsUserOnlineTool} from './clawly-is-user-online'
|
|
4
4
|
import {registerMsgBreakTool} from './clawly-msg-break'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
registerDeepSearchTool,
|
|
7
|
+
registerGrokSearchTool,
|
|
8
|
+
registerKimiSearchTool,
|
|
9
|
+
registerSearchTool,
|
|
10
|
+
} from './clawly-search'
|
|
6
11
|
import {registerSendAppPushTool} from './clawly-send-app-push'
|
|
7
12
|
import {registerSendFileTool} from './clawly-send-file'
|
|
8
13
|
import {registerSendMessageTool} from './clawly-send-message'
|
|
@@ -14,6 +19,7 @@ export function registerTools(api: PluginApi) {
|
|
|
14
19
|
registerSearchTool(api)
|
|
15
20
|
registerDeepSearchTool(api)
|
|
16
21
|
registerGrokSearchTool(api)
|
|
22
|
+
registerKimiSearchTool(api)
|
|
17
23
|
registerSendAppPushTool(api)
|
|
18
24
|
registerSendFileTool(api)
|
|
19
25
|
registerSendMessageTool(api)
|