Files
LiJian dbc1e47c1b ✨ feat(skill-store): route skill links to the Skill Store instead of the crawler (#19331)
* 🐛 fix(skill-store): route LobeHub skill URLs to importFromMarket instead of the crawler

A `lobehub.com/skills/{identifier}/skill.md` link in a user message was reaching
the model with no turn-local guidance. The only rule covering the case lived in
the activator's system role, far from the current turn — and the page, once
crawled, carries its own "always use the CLI, do NOT make raw API requests"
instructions plus a full `npx @lobehub/market-cli register && skills install`
walkthrough. Fresher and more specific won: the model crawled the page and went
to the sandbox, when the identifier it needed was already in the URL path.

- Add `SkillImportRouteInjector`: extracts the marketplace identifier from any
  LobeHub skill URL in the current user message and appends an explicit
  instruction to import it via `lobe-skill-store`, stating that it takes
  precedence over install steps found on the linked page. Gated on the Skill
  Store being enabled or listed as available.
- `SkillStoreManifest.meta.description` now carries the URL → `importFromMarket`
  rule. Before activation this single line is the only thing the model sees
  about the tool — its `systemRole` is not injected yet — so a keyword list
  alone left it outweighed by every already-active tool.
- Skill Store `systemRole`: install from the URL you were given rather than
  browsing to it first; never run marketplace CLI commands; never conclude a
  skill is missing from an empty `searchSkill` (matching is textual, so
  "pptx presentation slides" misses a skill named "pptx"); keep queries short.
- Activator `systemRole`: generic-tool fallback now requires that no skill URL
  or identifier was given, instead of firing whenever a search came back empty;
  ban marketplace CLI commands; `<available_tools>` is described as injected
  system context rather than a section of the system prompt, which is where it
  actually lands (`ToolDiscoveryProvider` extends `BaseFirstUserContentProvider`).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BZvgLyect6csrBufui73ZE

* ♻️ refactor(skill-store): make the CLI a ranked last resort, not a flat ban

The first pass banned the marketplace CLI outright. That overshoots: the CLI is
the legitimate path for an agent that has no Skill Store tool, and an absolute
"never" would strand it. The real defect was ordering, not the CLI itself.

Replace the prohibition with an explicit ladder — importFromMarket → importSkill
→ CLI — stated in all four places the model can read it (the turn-local
injection, the pre-activation manifest description, and both system roles), with
the reason the CLI sits last: it needs a device registration the tools don't, is
rate-limited 5 per 30 minutes per IP, and needs a working sandbox. Falling
through to it now requires having tried the steps above and saying what failed.

Also keeps the rebuttal to the phrase that caused the original detour — "install
it as documented" does not promote the CLI, because a skill page documents the
CLI for agents that lack this tool.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BZvgLyect6csrBufui73ZE

* ✨ feat(skill-store): route any skill source to importSkill, not just marketplace URLs

The route only recognised `lobehub.com/skills/{identifier}`, which left out most
of what `importSkill` actually accepts — a SKILL.md on any host, a GitHub repo or
subdirectory, a ZIP package. Those URLs fell straight back to the crawler, which
is the failure this route exists to prevent.

Classify every URL in the message instead of matching one host:

- `lobehub.com/skills/{identifier}` → `importFromMarket` with the identifier
- a `SKILL.md` on any host, or a GitHub path with a `skills/` segment →
  `importSkill` type `url`; both are unambiguous, so no further signal is needed
- an ordinary GitHub repo or a `.zip` → `importSkill`, but only when the message
  also expresses install intent (en + zh). A repo link in a message about
  reviewing code must not be read as "install this skill"

Each detected entry now carries the resolved call, so the injected block names
`importFromMarket` with its identifier or `importSkill` with its url and type
rather than describing one shape. `metadata.skillImportRoute` reports urls, since
an identifier no longer exists for every route.

`lobehub.com/skills/skill.md` has no identifier to extract and now falls through
to `importSkill` rather than being dropped — it is still a SKILL.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BZvgLyect6csrBufui73ZE

* 🔒 fix(skill-store): drop trailing URL punctuation without a quadratic regex

CodeQL flagged `/[!,.:;?、。,:;?]+$/` as polynomial ReDoS on uncontrolled data,
and it is reachable. The pattern has an end anchor but no start anchor, so the
engine retries from every offset; on a URL ending in a long punctuation run
followed by one more character it does O(n^2) work. `!` and `.` are both inside
`URL_PATTERN`'s character class, so `https://example.com/` + 200k `!` + `a`
survives URL matching as a single token and reaches the replace — straight from
message text.

Replace it with a backward scan over a Set. Same result, linear, no backtracking.
The regression test uses that 200k input and asserts it completes well inside a
second; the previous implementation would not have finished.

Reviewed the file's other patterns while here — all either start-anchored against
a single URL or a linear scan, and the one nested quantifier
(`(?:[\w-]+\.)*` in the marketplace pattern) has a forced split because `[\w-]`
cannot match the `.` that terminates each repetition.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BZvgLyect6csrBufui73ZE

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 17:42:09 +08:00
..