crewly 1.20.131 → 1.20.133
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/constants.ts +49 -0
- package/config/index.ts +2 -0
- package/config/skills/agent/core/find-skill/SKILL.md +58 -0
- package/config/skills/agent/core/find-skill/execute.sh +67 -0
- package/config/skills/agent/core/find-skill/execute.test.sh +69 -0
- package/config/skills/agent/core/generate-pdf/SKILL.md +24 -29
- package/config/skills/agent/core/generate-pdf/execute.sh +22 -78
- package/config/skills/agent/core/generate-pdf/execute.test.sh +45 -0
- package/config/skills/agent/core/install-skill/SKILL.md +67 -0
- package/config/skills/agent/core/install-skill/execute.sh +80 -0
- package/config/skills/agent/core/install-skill/execute.test.sh +74 -0
- package/config/skills/agent/pdf-tools/SKILL.md +103 -0
- package/config/skills/agent/pdf-tools/execute.sh +301 -0
- package/config/skills/agent/pdf-tools/execute.test.sh +218 -0
- package/config/skills/agent/pdf-tools/instructions.md +14 -0
- package/config/skills/agent/pdf-tools/pdf_tools.py +268 -0
- package/config/skills/agent/pdf-tools/skill.json +103 -0
- package/config/skills/agent/pdf-tools/style.css +143 -0
- package/config/skills/agent/transcribe-audio/SKILL.md +42 -5
- package/config/skills/agent/transcribe-audio/execute.sh +44 -11
- package/config/skills/agent/transcribe-audio/execute.test.sh +90 -0
- package/config/skills/agent/transcribe-audio/install-whisper-cpp.sh +126 -0
- package/config/skills/agent/transcribe-audio/install-whisper-cpp.test.sh +69 -0
- package/config/skills/agent/transcribe-audio/skill.json +67 -6
- package/config/skills/marketplace-registry.ts +8 -1
- package/config/skills/registry.json +337 -3
- package/dist/backend/backend/src/constants.d.ts +71 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +96 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud-device-pairing.controller.d.ts +56 -0
- package/dist/backend/backend/src/controllers/cloud/cloud-device-pairing.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/cloud/cloud-device-pairing.controller.js +114 -0
- package/dist/backend/backend/src/controllers/cloud/cloud-device-pairing.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts +25 -0
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.js +73 -55
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.d.ts +3 -0
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.js +9 -0
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/skill-setup/skill-setup.controller.d.ts +49 -0
- package/dist/backend/backend/src/controllers/skill-setup/skill-setup.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/skill-setup/skill-setup.controller.js +185 -0
- package/dist/backend/backend/src/controllers/skill-setup/skill-setup.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/skill-setup/skill-setup.routes.d.ts +21 -0
- package/dist/backend/backend/src/controllers/skill-setup/skill-setup.routes.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/skill-setup/skill-setup.routes.js +29 -0
- package/dist/backend/backend/src/controllers/skill-setup/skill-setup.routes.js.map +1 -0
- package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.js +5 -0
- package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts +14 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +33 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.client.d.ts +132 -0
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.client.d.ts.map +1 -0
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.client.js +228 -0
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.client.js.map +1 -0
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.service.d.ts +115 -0
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.service.js +160 -0
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.service.js.map +1 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +8 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -1
- package/dist/backend/backend/src/services/marketplace/marketplace.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/marketplace/marketplace.service.js +11 -3
- package/dist/backend/backend/src/services/marketplace/marketplace.service.js.map +1 -1
- package/dist/backend/backend/src/services/skill-setup/skill-discovery.service.d.ts +281 -0
- package/dist/backend/backend/src/services/skill-setup/skill-discovery.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/skill-setup/skill-discovery.service.js +559 -0
- package/dist/backend/backend/src/services/skill-setup/skill-discovery.service.js.map +1 -0
- package/dist/backend/backend/src/services/skill-setup/skill-install-job.service.d.ts +243 -0
- package/dist/backend/backend/src/services/skill-setup/skill-install-job.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/skill-setup/skill-install-job.service.js +439 -0
- package/dist/backend/backend/src/services/skill-setup/skill-install-job.service.js.map +1 -0
- package/dist/backend/backend/src/services/skill-setup/skill-setup-manifest.d.ts +138 -0
- package/dist/backend/backend/src/services/skill-setup/skill-setup-manifest.d.ts.map +1 -0
- package/dist/backend/backend/src/services/skill-setup/skill-setup-manifest.js +281 -0
- package/dist/backend/backend/src/services/skill-setup/skill-setup-manifest.js.map +1 -0
- package/dist/backend/backend/src/services/skill-setup/skill-setup-runner.service.d.ts +338 -0
- package/dist/backend/backend/src/services/skill-setup/skill-setup-runner.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/skill-setup/skill-setup-runner.service.js +761 -0
- package/dist/backend/backend/src/services/skill-setup/skill-setup-runner.service.js.map +1 -0
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +6 -0
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
- package/dist/backend/backend/src/types/marketplace.types.d.ts +16 -1
- package/dist/backend/backend/src/types/marketplace.types.d.ts.map +1 -1
- package/dist/backend/backend/src/utils/inbound-file-hint.utils.d.ts +55 -0
- package/dist/backend/backend/src/utils/inbound-file-hint.utils.d.ts.map +1 -0
- package/dist/backend/backend/src/utils/inbound-file-hint.utils.js +78 -0
- package/dist/backend/backend/src/utils/inbound-file-hint.utils.js.map +1 -0
- package/dist/backend/build-info.json +2 -2
- package/dist/backend/config/constants.d.ts +48 -0
- package/dist/backend/config/constants.d.ts.map +1 -1
- package/dist/backend/config/constants.js +48 -0
- package/dist/backend/config/constants.js.map +1 -1
- package/dist/backend/config/index.d.ts +2 -2
- package/dist/backend/config/index.d.ts.map +1 -1
- package/dist/backend/config/index.js +2 -2
- package/dist/backend/config/index.js.map +1 -1
- package/dist/cli/backend/src/constants.d.ts +71 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +96 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts +14 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +33 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
- package/dist/cli/backend/src/services/cloud/cloud-device-pairing.client.d.ts +132 -0
- package/dist/cli/backend/src/services/cloud/cloud-device-pairing.client.d.ts.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-device-pairing.client.js +228 -0
- package/dist/cli/backend/src/services/cloud/cloud-device-pairing.client.js.map +1 -0
- package/dist/cli/backend/src/services/marketplace/marketplace.service.d.ts +101 -0
- package/dist/cli/backend/src/services/marketplace/marketplace.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/marketplace/marketplace.service.js +392 -0
- package/dist/cli/backend/src/services/marketplace/marketplace.service.js.map +1 -0
- package/dist/cli/backend/src/services/skill-setup/skill-discovery.service.d.ts +281 -0
- package/dist/cli/backend/src/services/skill-setup/skill-discovery.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/skill-setup/skill-discovery.service.js +559 -0
- package/dist/cli/backend/src/services/skill-setup/skill-discovery.service.js.map +1 -0
- package/dist/cli/backend/src/services/skill-setup/skill-setup-manifest.d.ts +138 -0
- package/dist/cli/backend/src/services/skill-setup/skill-setup-manifest.d.ts.map +1 -0
- package/dist/cli/backend/src/services/skill-setup/skill-setup-manifest.js +281 -0
- package/dist/cli/backend/src/services/skill-setup/skill-setup-manifest.js.map +1 -0
- package/dist/cli/backend/src/services/skill-setup/skill-setup-runner.service.d.ts +338 -0
- package/dist/cli/backend/src/services/skill-setup/skill-setup-runner.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/skill-setup/skill-setup-runner.service.js +761 -0
- package/dist/cli/backend/src/services/skill-setup/skill-setup-runner.service.js.map +1 -0
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js +6 -0
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
- package/dist/cli/backend/src/types/marketplace.types.d.ts +219 -0
- package/dist/cli/backend/src/types/marketplace.types.d.ts.map +1 -0
- package/dist/cli/backend/src/types/marketplace.types.js +6 -0
- package/dist/cli/backend/src/types/marketplace.types.js.map +1 -0
- package/dist/cli/backend/src/utils/inbound-file-hint.utils.d.ts +55 -0
- package/dist/cli/backend/src/utils/inbound-file-hint.utils.d.ts.map +1 -0
- package/dist/cli/backend/src/utils/inbound-file-hint.utils.js +78 -0
- package/dist/cli/backend/src/utils/inbound-file-hint.utils.js.map +1 -0
- package/dist/cli/backend/src/utils/package-root.d.ts +15 -0
- package/dist/cli/backend/src/utils/package-root.d.ts.map +1 -0
- package/dist/cli/backend/src/utils/package-root.js +38 -0
- package/dist/cli/backend/src/utils/package-root.js.map +1 -0
- package/dist/cli/cli/src/commands/cloud.d.ts +45 -25
- package/dist/cli/cli/src/commands/cloud.d.ts.map +1 -1
- package/dist/cli/cli/src/commands/cloud.js +153 -47
- package/dist/cli/cli/src/commands/cloud.js.map +1 -1
- package/dist/cli/cli/src/commands/install.d.ts +13 -1
- package/dist/cli/cli/src/commands/install.d.ts.map +1 -1
- package/dist/cli/cli/src/commands/install.js +40 -3
- package/dist/cli/cli/src/commands/install.js.map +1 -1
- package/dist/cli/cli/src/commands/onboard-checklist.js +3 -3
- package/dist/cli/cli/src/commands/onboard-checklist.js.map +1 -1
- package/dist/cli/cli/src/commands/skills.d.ts +58 -0
- package/dist/cli/cli/src/commands/skills.d.ts.map +1 -0
- package/dist/cli/cli/src/commands/skills.js +132 -0
- package/dist/cli/cli/src/commands/skills.js.map +1 -0
- package/dist/cli/cli/src/index.js +18 -7
- package/dist/cli/cli/src/index.js.map +1 -1
- package/dist/cli/cli/src/utils/archive-creator.d.ts +14 -0
- package/dist/cli/cli/src/utils/archive-creator.d.ts.map +1 -1
- package/dist/cli/cli/src/utils/archive-creator.js +24 -3
- package/dist/cli/cli/src/utils/archive-creator.js.map +1 -1
- package/dist/cli/cli/src/utils/cloud-submit.d.ts.map +1 -1
- package/dist/cli/cli/src/utils/cloud-submit.js +14 -5
- package/dist/cli/cli/src/utils/cloud-submit.js.map +1 -1
- package/dist/cli/cli/src/utils/package-validator.d.ts +2 -0
- package/dist/cli/cli/src/utils/package-validator.d.ts.map +1 -1
- package/dist/cli/cli/src/utils/package-validator.js +16 -0
- package/dist/cli/cli/src/utils/package-validator.js.map +1 -1
- package/dist/cli/config/constants.d.ts +48 -0
- package/dist/cli/config/constants.d.ts.map +1 -1
- package/dist/cli/config/constants.js +48 -0
- package/dist/cli/config/constants.js.map +1 -1
- package/dist/cli/config/index.d.ts +2 -2
- package/dist/cli/config/index.d.ts.map +1 -1
- package/dist/cli/config/index.js +2 -2
- package/dist/cli/config/index.js.map +1 -1
- package/frontend/dist/assets/{index-5b98bf77.js → index-e6c403b6.js} +265 -265
- package/frontend/dist/index.html +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# install-skill — install a skill and its dependencies as a background job.
|
|
4
|
+
#
|
|
5
|
+
# Returns at once with a job id (or "already-ready"). When the job ends you get
|
|
6
|
+
# a message: [SKILL INSTALLED] … or [SKILL INSTALL FAILED] … with the reason.
|
|
7
|
+
# Tell the user in one line that you are installing it, then continue with
|
|
8
|
+
# other work or wait; resume the paused task when the message arrives.
|
|
9
|
+
# Backed by POST /api/skill-setup/install.
|
|
10
|
+
#
|
|
11
|
+
# Only official skills (bundled with Crewly, or published by the Crewly Team in
|
|
12
|
+
# the official registry) install without asking. A third-party skill needs the
|
|
13
|
+
# owner's yes in chat first; then pass --approved-by-owner (it is checked
|
|
14
|
+
# against the owner's chat history — claiming it without their yes fails).
|
|
15
|
+
#
|
|
16
|
+
# Usage:
|
|
17
|
+
# bash execute.sh --id transcribe-audio --resume "transcribe clip.m4a for Steve"
|
|
18
|
+
# bash execute.sh --id some-skill --approved-by-owner --owner-said "好的,装 some-skill"
|
|
19
|
+
# bash execute.sh '{"id":"pdf-tools","resume":"…"}'
|
|
20
|
+
# =============================================================================
|
|
21
|
+
set -euo pipefail
|
|
22
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
23
|
+
source "${SCRIPT_DIR}/../../_common/lib.sh"
|
|
24
|
+
|
|
25
|
+
print_usage() {
|
|
26
|
+
cat <<'EOF_USAGE'
|
|
27
|
+
Usage:
|
|
28
|
+
bash execute.sh --id <skill-id> [--resume "what you were doing"] [--approved-by-owner --owner-said "…"] [--force]
|
|
29
|
+
bash execute.sh '{"id":"…","resume":"…","approvedByOwner":false}'
|
|
30
|
+
|
|
31
|
+
Options:
|
|
32
|
+
--id Skill id from find-skill (required)
|
|
33
|
+
--resume What you were doing; echoed back in the completion message
|
|
34
|
+
--approved-by-owner The owner said yes in chat (third-party skills only; verified)
|
|
35
|
+
--owner-said Quote the owner's yes (recorded next to the verified message)
|
|
36
|
+
--force Re-run setup even if everything looks installed
|
|
37
|
+
--help | -h Show this help
|
|
38
|
+
EOF_USAGE
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
INPUT_JSON=""; ID=""; RESUME=""; APPROVED=false; FORCE=false; OWNER_SAID=""
|
|
42
|
+
if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then INPUT_JSON="$1"; shift || true; fi
|
|
43
|
+
while [[ $# -gt 0 ]]; do
|
|
44
|
+
case "$1" in
|
|
45
|
+
--id) [ $# -ge 2 ] || error_exit "--id requires a value"; ID="$2"; shift 2 ;;
|
|
46
|
+
--resume) [ $# -ge 2 ] || error_exit "--resume requires a value"; RESUME="$2"; shift 2 ;;
|
|
47
|
+
--approved-by-owner) APPROVED=true; shift ;;
|
|
48
|
+
--owner-said) [ $# -ge 2 ] || error_exit "--owner-said requires a value"; OWNER_SAID="$2"; shift 2 ;;
|
|
49
|
+
--force) FORCE=true; shift ;;
|
|
50
|
+
--help|-h) print_usage; exit 0 ;;
|
|
51
|
+
*) error_exit "Unknown option: $1" ;;
|
|
52
|
+
esac
|
|
53
|
+
done
|
|
54
|
+
if [ -n "$INPUT_JSON" ]; then
|
|
55
|
+
INPUT=$(read_json_input "$INPUT_JSON")
|
|
56
|
+
[ -z "$ID" ] && ID=$(printf '%s' "$INPUT" | jq -r '.id // empty')
|
|
57
|
+
[ -z "$RESUME" ] && RESUME=$(printf '%s' "$INPUT" | jq -r '.resume // empty')
|
|
58
|
+
[ "$(printf '%s' "$INPUT" | jq -r '.approvedByOwner // false')" = "true" ] && APPROVED=true
|
|
59
|
+
[ "$(printf '%s' "$INPUT" | jq -r '.force // false')" = "true" ] && FORCE=true
|
|
60
|
+
fi
|
|
61
|
+
require_param "id (--id)" "$ID"
|
|
62
|
+
# The quote travels as X-Agent-Authorization (lib.sh encodes it): recorded, never trusted.
|
|
63
|
+
[ -n "$OWNER_SAID" ] && export CREWLY_AGENT_AUTHORIZATION="$OWNER_SAID"
|
|
64
|
+
|
|
65
|
+
BODY=$(jq -cn --arg id "$ID" --arg resume "$RESUME" --argjson approved "$APPROVED" --argjson force "$FORCE" \
|
|
66
|
+
'{id: $id} + (if $resume != "" then {resume: $resume} else {} end)
|
|
67
|
+
+ (if $approved then {approvedByOwner: true} else {} end) + (if $force then {force: true} else {} end)')
|
|
68
|
+
|
|
69
|
+
ERR_FILE=$(mktemp); trap 'rm -f "$ERR_FILE"' EXIT
|
|
70
|
+
RESPONSE=$(api_call POST "/skill-setup/install" "$BODY" 2>"$ERR_FILE") || {
|
|
71
|
+
ERR=$(tail -n 1 "$ERR_FILE")
|
|
72
|
+
printf '%s' "$ERR" | jq -c '{success: false,
|
|
73
|
+
reason: (.details.code // "install_failed"),
|
|
74
|
+
error: (.details.error // .error // "install failed")}
|
|
75
|
+
+ (if (.details.code // "") | startswith("owner_approval") then {official: false, next: "Ask the owner in chat before installing this third-party skill."} else {} end)' 2>/dev/null \
|
|
76
|
+
|| jq -cn --arg e "$ERR" '{success: false, reason: "install_failed", error: $e}'
|
|
77
|
+
exit 1
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
printf '%s' "$RESPONSE" | jq -c '.data | {success: true} + .'
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for install-skill — run with: bash execute.test.sh </dev/null
|
|
3
|
+
# A python HTTP stub plays the backend; asserts the request body, the
|
|
4
|
+
# X-Agent-Authorization citation and how refusals are reported.
|
|
5
|
+
set -u
|
|
6
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
EXEC="$HERE/execute.sh"
|
|
8
|
+
PASS=0; FAIL=0
|
|
9
|
+
check() {
|
|
10
|
+
local name="$1"; local got="$2"; local want="$3"
|
|
11
|
+
if [ "$got" = "$want" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $name"; echo " want: $want"; echo " got: $got"; fi
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
PORT=18852
|
|
15
|
+
STUB_LOG="$(mktemp)"; export STUB_LOG; export STUB_PORT="$PORT"
|
|
16
|
+
python3 -u <<'PY' >/dev/null 2>&1 &
|
|
17
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
18
|
+
import base64, json, os
|
|
19
|
+
LOG = os.environ['STUB_LOG']; PORT = int(os.environ['STUB_PORT'])
|
|
20
|
+
class H(BaseHTTPRequestHandler):
|
|
21
|
+
def reply(self, code, body):
|
|
22
|
+
self.send_response(code); self.send_header('Content-Type', 'application/json'); self.end_headers()
|
|
23
|
+
self.wfile.write(json.dumps(body).encode())
|
|
24
|
+
def do_POST(self):
|
|
25
|
+
body = json.loads(self.rfile.read(int(self.headers.get('Content-Length', 0))) or b'{}')
|
|
26
|
+
auth = self.headers.get('X-Agent-Authorization')
|
|
27
|
+
if auth and auth.startswith('b64:'):
|
|
28
|
+
auth = base64.b64decode(auth[4:]).decode()
|
|
29
|
+
open(LOG, 'w').write(json.dumps({'path': self.path, 'body': body, 'agent': self.headers.get('X-Agent-Session'), 'auth': auth}))
|
|
30
|
+
if body['id'] == 'shady-ocr' and not body.get('approvedByOwner'):
|
|
31
|
+
return self.reply(403, {'success': False, 'code': 'owner_approval_required', 'error': '"shady-ocr" is a third-party skill. Ask the owner in chat…'})
|
|
32
|
+
if body['id'] == 'ready-one':
|
|
33
|
+
return self.reply(200, {'success': True, 'data': {'state': 'already-ready', 'skillId': 'ready-one', 'executePath': '/x/execute.sh', 'next': 'use it now'}})
|
|
34
|
+
self.reply(202, {'success': True, 'data': {'jobId': 'job1', 'skillId': body['id'], 'state': 'running', 'official': True,
|
|
35
|
+
'estimatedMinutes': 6, 'willNotify': ['test-agent'], 'next': 'Installing…'}})
|
|
36
|
+
def log_message(self, *a, **k): pass
|
|
37
|
+
HTTPServer(('127.0.0.1', PORT), H).serve_forever()
|
|
38
|
+
PY
|
|
39
|
+
STUB_PID=$!
|
|
40
|
+
disown "$STUB_PID" 2>/dev/null || true
|
|
41
|
+
trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -f "$STUB_LOG"' EXIT
|
|
42
|
+
for i in $(seq 1 30); do curl -s -o /dev/null -m 0.2 "http://127.0.0.1:${PORT}/" 2>/dev/null && break; sleep 0.1; done
|
|
43
|
+
|
|
44
|
+
run() { CREWLY_SESSION_NAME=test-agent CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>/dev/null; }
|
|
45
|
+
run_err() { CREWLY_SESSION_NAME=test-agent CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>&1 >/dev/null | jq -rs 'last.error' 2>/dev/null; }
|
|
46
|
+
|
|
47
|
+
OUT=$(run --id transcribe-audio --resume "transcribe clip.m4a for Steve")
|
|
48
|
+
check "path" "$(jq -r .path "$STUB_LOG")" "/api/skill-setup/install"
|
|
49
|
+
check "body" "$(jq -c .body "$STUB_LOG")" '{"id":"transcribe-audio","resume":"transcribe clip.m4a for Steve"}'
|
|
50
|
+
check "agent header (who gets the completion message)" "$(jq -r .agent "$STUB_LOG")" "test-agent"
|
|
51
|
+
check "no citation by default" "$(jq -r .auth "$STUB_LOG")" "null"
|
|
52
|
+
check "job returned at once" "$(printf '%s' "$OUT" | jq -c '{success, jobId, state, estimatedMinutes}')" '{"success":true,"jobId":"job1","state":"running","estimatedMinutes":6}'
|
|
53
|
+
|
|
54
|
+
OUT=$(run --id shady-ocr); RC=$?
|
|
55
|
+
check "third-party: exit 1" "$RC" "1"
|
|
56
|
+
check "third-party: reason" "$(printf '%s' "$OUT" | jq -r .reason)" "owner_approval_required"
|
|
57
|
+
check "third-party: next says ask the owner" "$(printf '%s' "$OUT" | jq -r .next)" "Ask the owner in chat before installing this third-party skill."
|
|
58
|
+
|
|
59
|
+
OUT=$(run --id shady-ocr --approved-by-owner --owner-said "好的,装 shady-ocr")
|
|
60
|
+
check "approved: flag sent" "$(jq -c .body "$STUB_LOG")" '{"id":"shady-ocr","approvedByOwner":true}'
|
|
61
|
+
check "approved: owner quote travels as X-Agent-Authorization" "$(jq -r .auth "$STUB_LOG")" "好的,装 shady-ocr"
|
|
62
|
+
check "approved: job" "$(printf '%s' "$OUT" | jq -r .jobId)" "job1"
|
|
63
|
+
|
|
64
|
+
OUT=$(run '{"id":"pdf-tools","force":true}')
|
|
65
|
+
check "json input" "$(jq -c .body "$STUB_LOG")" '{"id":"pdf-tools","force":true}'
|
|
66
|
+
|
|
67
|
+
OUT=$(run --id ready-one)
|
|
68
|
+
check "already ready" "$(printf '%s' "$OUT" | jq -c '{state, executePath}')" '{"state":"already-ready","executePath":"/x/execute.sh"}'
|
|
69
|
+
|
|
70
|
+
check "missing id" "$(run_err)" "Missing required parameter: id (--id)"
|
|
71
|
+
check "unknown option" "$(run_err --yes)" "Unknown option: --yes"
|
|
72
|
+
|
|
73
|
+
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
74
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pdf-tools
|
|
3
|
+
description: Make polished PDFs from Markdown or styled HTML (headless Chrome preferred, WeasyPrint fallback, CJK-ready default stylesheet), merge several parts into one PDF, and read the text of a PDF someone sent you. When a dependency is missing it returns `needsSetup:true` — run `install-skill --id pdf-tools`.
|
|
4
|
+
category: content-creation
|
|
5
|
+
assignableRoles:
|
|
6
|
+
- "*"
|
|
7
|
+
version: "1.0.0"
|
|
8
|
+
author: Crewly Team
|
|
9
|
+
tags:
|
|
10
|
+
- pdf
|
|
11
|
+
- document
|
|
12
|
+
- report
|
|
13
|
+
- markdown
|
|
14
|
+
- html
|
|
15
|
+
- chrome
|
|
16
|
+
- weasyprint
|
|
17
|
+
- merge
|
|
18
|
+
- read-pdf
|
|
19
|
+
- extract-text
|
|
20
|
+
- pypdf
|
|
21
|
+
- cjk
|
|
22
|
+
- 中文
|
|
23
|
+
- 文档
|
|
24
|
+
triggers:
|
|
25
|
+
- make a pdf
|
|
26
|
+
- generate pdf
|
|
27
|
+
- markdown to pdf
|
|
28
|
+
- html to pdf
|
|
29
|
+
- merge pdfs
|
|
30
|
+
- read this pdf
|
|
31
|
+
- extract text from pdf
|
|
32
|
+
execution:
|
|
33
|
+
type: script
|
|
34
|
+
script:
|
|
35
|
+
file: execute.sh
|
|
36
|
+
interpreter: bash
|
|
37
|
+
timeoutMs: 300000
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
# PDF Tools
|
|
41
|
+
|
|
42
|
+
Make PDFs people like to read, and read the PDFs people send you.
|
|
43
|
+
|
|
44
|
+
## Make a PDF (`render`)
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Markdown → PDF with the default A4 stylesheet (CJK fonts, tables, code, quotes)
|
|
48
|
+
bash {{AGENT_SKILLS_PATH}}/pdf-tools/execute.sh '{"action":"render","input":"/tmp/report.md","output":"/tmp/report.pdf","title":"Q3 报告"}'
|
|
49
|
+
|
|
50
|
+
# Your own styled HTML → PDF (kept exactly as you styled it)
|
|
51
|
+
bash {{AGENT_SKILLS_PATH}}/pdf-tools/execute.sh '{"action":"render","input":"/tmp/brief.html","output":"/tmp/brief.pdf"}'
|
|
52
|
+
|
|
53
|
+
# Several parts → one PDF, in order (.md / .html / .pdf)
|
|
54
|
+
bash {{AGENT_SKILLS_PATH}}/pdf-tools/execute.sh '{"action":"render","inputs":["/tmp/cover.html","/tmp/body.md","/tmp/appendix.pdf"],"output":"/tmp/full.pdf"}'
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
| Field | Default | Meaning |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| `input` / `inputs` | — | One file, or a list of parts merged in order |
|
|
60
|
+
| `output` | input path with `.pdf` | Where the PDF goes (required for several parts) |
|
|
61
|
+
| `title` | file name | Document title (Markdown only) |
|
|
62
|
+
| `css` | — | Extra stylesheet appended after the default one |
|
|
63
|
+
| `addDefaultStyle` | `false` | Also apply the default stylesheet to HTML input |
|
|
64
|
+
| `engine` | `auto` | `auto` (Chrome, else WeasyPrint), `chrome`, `weasyprint` |
|
|
65
|
+
| `lang` | `zh` | `lang` attribute of the generated HTML (font selection) |
|
|
66
|
+
|
|
67
|
+
**Engines.** Headless Chrome/Chromium renders exactly what a browser shows
|
|
68
|
+
(modern CSS, web fonts, CJK with system fonts) and is used whenever it is
|
|
69
|
+
installed. WeasyPrint is the fallback. Markdown goes through python-markdown
|
|
70
|
+
(or pandoc when the venv is missing).
|
|
71
|
+
|
|
72
|
+
**Writing HTML for print.** Use `@page { size: A4; margin: 18mm }`, and
|
|
73
|
+
`<div class="page-break"></div>` (default stylesheet) or `break-after: page` to
|
|
74
|
+
start a new page. For a cover page, render it as its own part and list it first.
|
|
75
|
+
|
|
76
|
+
Output: `{"success":true,"pdf":"/tmp/report.pdf","size":48213,"parts":1,"engine":"chrome"}`
|
|
77
|
+
|
|
78
|
+
## Read a PDF (`read`)
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
bash {{AGENT_SKILLS_PATH}}/pdf-tools/execute.sh '{"action":"read","input":"/path/to/file.pdf"}'
|
|
82
|
+
bash {{AGENT_SKILLS_PATH}}/pdf-tools/execute.sh '{"action":"read","input":"/path/to/file.pdf","pages":"1-3,7","textFile":"/tmp/file.txt"}'
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Returns `{success, engine, pages, pagesRead, chars, text, truncated, textFile?}`.
|
|
86
|
+
Text is split with `--- page N ---` markers. Long documents are cut at
|
|
87
|
+
`maxChars` (default 20000) — pass `textFile` to get everything on disk. A `note`
|
|
88
|
+
says when the PDF has no text layer (scanned images).
|
|
89
|
+
|
|
90
|
+
## Other actions
|
|
91
|
+
|
|
92
|
+
- `merge` — `{"action":"merge","inputs":["a.pdf","b.pdf"],"output":"ab.pdf"}`
|
|
93
|
+
- `info` — `{"action":"info","input":"x.pdf"}` → pages, metadata, encrypted
|
|
94
|
+
- `check` — which engines exist here (`canRender`, `canRead`, `canMerge`)
|
|
95
|
+
|
|
96
|
+
## Setup
|
|
97
|
+
|
|
98
|
+
Declared in `skill.json` → `setup`; `install-skill --id pdf-tools` (agents) or
|
|
99
|
+
`crewly skills setup pdf-tools` (terminal) installs what is missing:
|
|
100
|
+
Python venv `~/.crewly/venv/pdf-tools` with pypdf, markdown and weasyprint;
|
|
101
|
+
optionally Chrome (Linux: `chromium`), Pango for WeasyPrint and Noto CJK fonts.
|
|
102
|
+
A missing dependency fails with `"needsSetup": true, "skill": "pdf-tools"` —
|
|
103
|
+
tell the user you are setting it up and run `install-skill`, don't give up.
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# pdf-tools — make and read PDFs.
|
|
4
|
+
#
|
|
5
|
+
# render : Markdown / HTML (one or several parts) → one PDF.
|
|
6
|
+
# Engine: headless Chrome/Chromium (preferred) → WeasyPrint (fallback).
|
|
7
|
+
# Markdown gets a clean A4 stylesheet with CJK fonts (style.css).
|
|
8
|
+
# merge : concatenate PDFs (pypdf).
|
|
9
|
+
# read : extract text from a PDF (pypdf, falls back to pdftotext).
|
|
10
|
+
# info : page count + metadata.
|
|
11
|
+
# check : which engines are available here.
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# bash execute.sh '{"action":"render","input":"/tmp/report.md","output":"/tmp/report.pdf","title":"Q3 报告"}'
|
|
15
|
+
# bash execute.sh '{"action":"render","inputs":["cover.html","body.md","appendix.pdf"],"output":"/tmp/all.pdf"}'
|
|
16
|
+
# bash execute.sh '{"action":"read","input":"/tmp/contract.pdf","pages":"1-3"}'
|
|
17
|
+
# bash execute.sh '{"action":"merge","inputs":["a.pdf","b.pdf"],"output":"/tmp/ab.pdf"}'
|
|
18
|
+
# bash execute.sh --file /tmp/input.json (JSON in a file)
|
|
19
|
+
#
|
|
20
|
+
# Dependencies are declared in skill.json → setup (install-skill --id pdf-tools).
|
|
21
|
+
# A missing dependency fails with "needsSetup": true, "skill": "pdf-tools".
|
|
22
|
+
#
|
|
23
|
+
# Test hooks: PDF_TOOLS_PYTHON (interpreter), CHROME_BIN (browser),
|
|
24
|
+
# PDF_TOOLS_CHROME_PATHS (browser paths searched before PATH, colon separated).
|
|
25
|
+
# =============================================================================
|
|
26
|
+
set -euo pipefail
|
|
27
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
28
|
+
SKILL_ID="pdf-tools"
|
|
29
|
+
HELPER="${SCRIPT_DIR}/pdf_tools.py"
|
|
30
|
+
DEFAULT_CSS="${SCRIPT_DIR}/style.css"
|
|
31
|
+
CREWLY_HOME_DIR="${CREWLY_HOME:-${HOME}/.crewly}"
|
|
32
|
+
VENV_PYTHON="${CREWLY_HOME_DIR}/venv/${SKILL_ID}/bin/python3"
|
|
33
|
+
CHROME_TIMEOUT_SEC="${PDF_TOOLS_CHROME_TIMEOUT_SEC:-120}"
|
|
34
|
+
CHROME_EXIT_GRACE_SEC=3
|
|
35
|
+
CHROME_APP_PATHS="${PDF_TOOLS_CHROME_PATHS-/Applications/Google Chrome.app/Contents/MacOS/Google Chrome:${HOME}/Applications/Google Chrome.app/Contents/MacOS/Google Chrome:/Applications/Chromium.app/Contents/MacOS/Chromium:/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge}"
|
|
36
|
+
CHROME_COMMANDS="google-chrome google-chrome-stable chromium chromium-browser microsoft-edge"
|
|
37
|
+
|
|
38
|
+
json_str() { printf '%s' "$1" | jq -Rsa .; }
|
|
39
|
+
err_json() { printf '{"success":false,"error":%s}\n' "$(json_str "$1")"; exit 1; }
|
|
40
|
+
needs_setup() {
|
|
41
|
+
# needs_setup <message> <missing...>
|
|
42
|
+
local msg="$1"; shift
|
|
43
|
+
jq -cn --arg e "$msg" --arg s "$SKILL_ID" --args '{success:false,error:$e,needsSetup:true,skill:$s,missing:$ARGS.positional,
|
|
44
|
+
hint:("Run install-skill --id " + $s + " (installs in the background and messages you when done), then retry.")}' "$@"
|
|
45
|
+
exit 1
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
command -v jq >/dev/null 2>&1 || {
|
|
49
|
+
printf '{"success":false,"error":"jq is required but not installed","needsSetup":true,"skill":"%s","missing":["jq"]}\n' "$SKILL_ID"; exit 1; }
|
|
50
|
+
|
|
51
|
+
# ── Input: JSON argument, --file <path>, or stdin ────────────────────────────────
|
|
52
|
+
INPUT=""
|
|
53
|
+
if [ "${1:-}" = "--file" ] && [ -n "${2:-}" ]; then
|
|
54
|
+
[ -f "$2" ] || err_json "File not found: $2"
|
|
55
|
+
INPUT="$(cat "$2")"
|
|
56
|
+
elif [ -n "${1:-}" ]; then
|
|
57
|
+
INPUT="$1"
|
|
58
|
+
elif [ ! -t 0 ]; then
|
|
59
|
+
INPUT="$(cat)"
|
|
60
|
+
fi
|
|
61
|
+
[ -z "$INPUT" ] && err_json "Usage: execute.sh '{\"action\":\"render\",\"input\":\"/tmp/report.md\"}' (actions: render, merge, read, info, check)"
|
|
62
|
+
printf '%s' "$INPUT" | jq -e 'type == "object"' >/dev/null 2>&1 || err_json "input must be a JSON object"
|
|
63
|
+
|
|
64
|
+
field() { printf '%s' "$INPUT" | jq -r "$1 // empty"; }
|
|
65
|
+
ACTION="$(field .action)"; [ -z "$ACTION" ] && ACTION="render"
|
|
66
|
+
|
|
67
|
+
WORK="$(mktemp -d "${TMPDIR:-/tmp}/pdf-tools-XXXXXX")"
|
|
68
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
69
|
+
|
|
70
|
+
# ── Tool discovery ────────────────────────────────────────────────────────────────
|
|
71
|
+
# Python with the skill's packages: override → skill venv → system python3
|
|
72
|
+
# (stdlib only; enough for `read` via pdftotext).
|
|
73
|
+
python_bin() {
|
|
74
|
+
if [ -n "${PDF_TOOLS_PYTHON:-}" ]; then echo "$PDF_TOOLS_PYTHON"; return; fi
|
|
75
|
+
if [ -x "$VENV_PYTHON" ]; then echo "$VENV_PYTHON"; return; fi
|
|
76
|
+
command -v python3 2>/dev/null || true
|
|
77
|
+
}
|
|
78
|
+
py_has() { local py; py="$(python_bin)"; [ -n "$py" ] && "$py" -c "import $1" >/dev/null 2>&1; }
|
|
79
|
+
|
|
80
|
+
find_chrome() {
|
|
81
|
+
if [ -n "${CHROME_BIN:-}" ]; then [ -x "$CHROME_BIN" ] && echo "$CHROME_BIN"; return; fi
|
|
82
|
+
local IFS=':' p c
|
|
83
|
+
for p in $CHROME_APP_PATHS; do [ -n "$p" ] && [ -x "$p" ] && { echo "$p"; return; }; done
|
|
84
|
+
IFS=' '
|
|
85
|
+
for c in $CHROME_COMMANDS; do command -v "$c" >/dev/null 2>&1 && { command -v "$c"; return; }; done
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
weasy_available() { py_has weasyprint || command -v weasyprint >/dev/null 2>&1; }
|
|
89
|
+
|
|
90
|
+
file_url() { printf 'file://%s' "$(printf '%s' "$1" | jq -sRr @uri | sed 's/%2F/\//g')"; }
|
|
91
|
+
|
|
92
|
+
abs_path() { case "$1" in /*) printf '%s' "$1" ;; *) printf '%s/%s' "$(pwd)" "$1" ;; esac; }
|
|
93
|
+
|
|
94
|
+
is_pdf() { [ -s "$1" ] && [ "$(head -c 4 "$1")" = "%PDF" ]; }
|
|
95
|
+
|
|
96
|
+
# ── Engines ───────────────────────────────────────────────────────────────────────
|
|
97
|
+
# A PDF Chrome has finished writing: starts with %PDF, ends with %%EOF.
|
|
98
|
+
pdf_complete() { is_pdf "$1" && tail -c 64 "$1" | grep -q '%%EOF'; }
|
|
99
|
+
|
|
100
|
+
# Headless print. No --user-data-dir: with a fresh profile dir Chrome 15x on
|
|
101
|
+
# macOS writes the PDF and then never exits. As a guard against any other
|
|
102
|
+
# hang, once the PDF is complete Chrome gets CHROME_EXIT_GRACE_SEC to exit
|
|
103
|
+
# before it is stopped; CHROME_TIMEOUT_SEC bounds the whole run.
|
|
104
|
+
render_chrome() {
|
|
105
|
+
local html="$1" out="$2" chrome="$3"
|
|
106
|
+
local args=(--headless=new --disable-gpu --no-pdf-header-footer --no-first-run --no-default-browser-check
|
|
107
|
+
--disable-extensions --run-all-compositor-stages-before-draw "--print-to-pdf=${out}")
|
|
108
|
+
# Chrome refuses to run as root without this (servers / containers).
|
|
109
|
+
[ "$(id -u)" = "0" ] && args+=(--no-sandbox)
|
|
110
|
+
rm -f "$out"
|
|
111
|
+
"$chrome" "${args[@]}" "$(file_url "$html")" </dev/null >"${WORK}/cmd.out" 2>&1 &
|
|
112
|
+
local pid=$! waited=0 done_at=-1
|
|
113
|
+
while kill -0 "$pid" 2>/dev/null; do
|
|
114
|
+
if [ "$done_at" -lt 0 ] && pdf_complete "$out"; then done_at=$waited; fi
|
|
115
|
+
if [ "$waited" -ge "$((CHROME_TIMEOUT_SEC * 2))" ] || { [ "$done_at" -ge 0 ] && [ "$((waited - done_at))" -ge "$((CHROME_EXIT_GRACE_SEC * 2))" ]; }; then
|
|
116
|
+
kill -9 "$pid" 2>/dev/null || true
|
|
117
|
+
break
|
|
118
|
+
fi
|
|
119
|
+
sleep 0.5
|
|
120
|
+
waited=$((waited + 1))
|
|
121
|
+
done
|
|
122
|
+
wait "$pid" 2>/dev/null || true
|
|
123
|
+
pdf_complete "$out"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
render_weasy() {
|
|
127
|
+
local html="$1" out="$2" py
|
|
128
|
+
py="$(python_bin)"
|
|
129
|
+
if [ -n "$py" ] && "$py" -c "import weasyprint" >/dev/null 2>&1; then
|
|
130
|
+
"$py" "$HELPER" weasyprint "$html" "$out" >"${WORK}/weasy.out" 2>&1 || true
|
|
131
|
+
elif command -v weasyprint >/dev/null 2>&1; then
|
|
132
|
+
weasyprint "$html" "$out" >"${WORK}/weasy.out" 2>&1 || true
|
|
133
|
+
fi
|
|
134
|
+
is_pdf "$out"
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# html_to_pdf <html> <out> → sets USED_ENGINE
|
|
138
|
+
USED_ENGINE=""
|
|
139
|
+
html_to_pdf() {
|
|
140
|
+
local html="$1" out="$2" engine="$3" chrome
|
|
141
|
+
chrome="$(find_chrome || true)"
|
|
142
|
+
if [ "$engine" != "weasyprint" ] && [ -n "$chrome" ]; then
|
|
143
|
+
if render_chrome "$html" "$out" "$chrome"; then USED_ENGINE="chrome"; return 0; fi
|
|
144
|
+
[ "$engine" = "chrome" ] && err_json "Chrome failed to print ${html}: $(tail -c 300 "${WORK}/cmd.out" 2>/dev/null)"
|
|
145
|
+
echo '{"status":"fallback","message":"Chrome failed; trying WeasyPrint"}' >&2
|
|
146
|
+
elif [ "$engine" = "chrome" ]; then
|
|
147
|
+
needs_setup "engine=chrome requested but no Chrome/Chromium was found (set CHROME_BIN or install Chrome)" chrome
|
|
148
|
+
fi
|
|
149
|
+
if weasy_available; then
|
|
150
|
+
if render_weasy "$html" "$out"; then USED_ENGINE="weasyprint"; return 0; fi
|
|
151
|
+
err_json "WeasyPrint failed to render ${html}: $(tail -c 400 "${WORK}/weasy.out" 2>/dev/null)"
|
|
152
|
+
fi
|
|
153
|
+
needs_setup "No PDF engine is available: no Chrome/Chromium and no working WeasyPrint" chrome weasyprint-libs
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# md_to_html <md> <html> <title> — relative images resolve from the Markdown's
|
|
157
|
+
# own directory through a <base href>, so nothing is written next to it.
|
|
158
|
+
md_to_html() {
|
|
159
|
+
local md="$1" html="$2" title="$3" py base
|
|
160
|
+
base="$(file_url "$(dirname "$md")")/"
|
|
161
|
+
local args=("$HELPER" md2html "$md" "$html" --lang "$LANG_ATTR" --base "$base")
|
|
162
|
+
[ -n "$title" ] && args+=(--title "$title")
|
|
163
|
+
[ -f "$DEFAULT_CSS" ] && args+=(--css "$DEFAULT_CSS")
|
|
164
|
+
[ -n "$EXTRA_CSS" ] && args+=(--css "$EXTRA_CSS")
|
|
165
|
+
py="$(python_bin)"
|
|
166
|
+
if [ -n "$py" ] && "$py" -c "import markdown" >/dev/null 2>&1; then
|
|
167
|
+
"$py" "${args[@]}" >"${WORK}/md.out" 2>&1 || err_json "Markdown conversion failed: $(tail -c 300 "${WORK}/md.out")"
|
|
168
|
+
elif command -v pandoc >/dev/null 2>&1; then
|
|
169
|
+
local body; body="$(pandoc -f gfm -t html "$md")" || err_json "pandoc failed on ${md}"
|
|
170
|
+
{
|
|
171
|
+
printf '<!DOCTYPE html>\n<html lang="%s"><head><meta charset="utf-8"><base href="%s"><title>%s</title><style>\n' "$LANG_ATTR" "$base" "$title"
|
|
172
|
+
[ -f "$DEFAULT_CSS" ] && cat "$DEFAULT_CSS"
|
|
173
|
+
[ -n "$EXTRA_CSS" ] && cat "$EXTRA_CSS"
|
|
174
|
+
printf '\n</style></head>\n<body><main class="doc">\n%s\n</main></body></html>\n' "$body"
|
|
175
|
+
} > "$html"
|
|
176
|
+
else
|
|
177
|
+
needs_setup "Markdown needs python-markdown (pdf-tools venv) or pandoc" python-packages
|
|
178
|
+
fi
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
# Copy an HTML file into the work dir with the default stylesheet (and the
|
|
182
|
+
# optional extra css) added first, so the document's own rules still win.
|
|
183
|
+
# A <base href> keeps its relative links pointing at the original directory.
|
|
184
|
+
style_html() {
|
|
185
|
+
local src="$1" dest="$2"
|
|
186
|
+
{
|
|
187
|
+
printf '<base href="%s/">\n<style>\n' "$(file_url "$(dirname "$src")")"
|
|
188
|
+
cat "$DEFAULT_CSS"
|
|
189
|
+
[ -n "$EXTRA_CSS" ] && cat "$EXTRA_CSS"
|
|
190
|
+
printf '\n</style>\n'
|
|
191
|
+
cat "$src"
|
|
192
|
+
} > "$dest"
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
run_helper() {
|
|
196
|
+
local py; py="$(python_bin)"
|
|
197
|
+
[ -n "$py" ] || needs_setup "python3 is not installed" python3
|
|
198
|
+
"$py" "$HELPER" "$@"
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
# ── Actions ───────────────────────────────────────────────────────────────────────
|
|
202
|
+
EXTRA_CSS="$(field .css)"
|
|
203
|
+
[ -n "$EXTRA_CSS" ] && [ ! -f "$EXTRA_CSS" ] && err_json "css file not found: ${EXTRA_CSS}"
|
|
204
|
+
LANG_ATTR="$(field .lang)"; [ -z "$LANG_ATTR" ] && LANG_ATTR="zh"
|
|
205
|
+
|
|
206
|
+
case "$ACTION" in
|
|
207
|
+
render)
|
|
208
|
+
ENGINE="$(field .engine)"; [ -z "$ENGINE" ] && ENGINE="auto"
|
|
209
|
+
case "$ENGINE" in auto|chrome|weasyprint) ;; *) err_json "engine must be auto, chrome or weasyprint (got: ${ENGINE})" ;; esac
|
|
210
|
+
TITLE="$(field .title)"
|
|
211
|
+
ADD_STYLE="$(printf '%s' "$INPUT" | jq -r '.addDefaultStyle // false')"
|
|
212
|
+
PARTS=()
|
|
213
|
+
while IFS= read -r p; do [ -n "$p" ] && PARTS+=("$p"); done < <(printf '%s' "$INPUT" | jq -r 'if (.inputs|type) == "array" then .inputs[] else (.input // empty) end')
|
|
214
|
+
[ ${#PARTS[@]} -eq 0 ] && err_json "input (a .md/.html file) or inputs (a list of .md/.html/.pdf parts) is required"
|
|
215
|
+
OUTPUT="$(field .output)"
|
|
216
|
+
if [ -z "$OUTPUT" ]; then
|
|
217
|
+
[ ${#PARTS[@]} -gt 1 ] && err_json "output is required when rendering several parts"
|
|
218
|
+
OUTPUT="${PARTS[0]%.*}.pdf"
|
|
219
|
+
fi
|
|
220
|
+
OUTPUT="$(abs_path "$OUTPUT")"
|
|
221
|
+
mkdir -p "$(dirname "$OUTPUT")"
|
|
222
|
+
PDF_PARTS=(); ENGINES=()
|
|
223
|
+
i=0
|
|
224
|
+
for part in "${PARTS[@]}"; do
|
|
225
|
+
i=$((i + 1))
|
|
226
|
+
[ -f "$part" ] || err_json "input not found: ${part}"
|
|
227
|
+
part="$(abs_path "$part")"
|
|
228
|
+
case "$(printf '%s' "${part##*.}" | tr '[:upper:]' '[:lower:]')" in
|
|
229
|
+
md|markdown)
|
|
230
|
+
html="${WORK}/part-${i}.html"
|
|
231
|
+
part_title="$TITLE"; [ -z "$part_title" ] && part_title="$(basename "${part%.*}")"
|
|
232
|
+
md_to_html "$part" "$html" "$part_title"
|
|
233
|
+
;;
|
|
234
|
+
html|htm)
|
|
235
|
+
html="$part"
|
|
236
|
+
if [ "$ADD_STYLE" = "true" ]; then
|
|
237
|
+
html="${WORK}/part-${i}.html"
|
|
238
|
+
style_html "$part" "$html"
|
|
239
|
+
fi
|
|
240
|
+
;;
|
|
241
|
+
pdf) PDF_PARTS+=("$part"); ENGINES+=("pdf"); continue ;;
|
|
242
|
+
*) err_json "unsupported input type: ${part} (use .md, .html or .pdf)" ;;
|
|
243
|
+
esac
|
|
244
|
+
out="${WORK}/part-${i}.pdf"; [ ${#PARTS[@]} -eq 1 ] && out="$OUTPUT"
|
|
245
|
+
html_to_pdf "$html" "$out" "$ENGINE"
|
|
246
|
+
PDF_PARTS+=("$out"); ENGINES+=("$USED_ENGINE")
|
|
247
|
+
done
|
|
248
|
+
if [ ${#PARTS[@]} -gt 1 ] || [ "${PDF_PARTS[0]}" != "$OUTPUT" ]; then
|
|
249
|
+
MERGED="$(run_helper merge "$OUTPUT" "${PDF_PARTS[@]}" 2>&1)" || {
|
|
250
|
+
printf '%s' "$MERGED" | jq -e .needsSetup >/dev/null 2>&1 && { printf '%s\n' "$MERGED"; exit 1; }
|
|
251
|
+
err_json "merging the parts failed: $(printf '%s' "$MERGED" | tail -c 300)"
|
|
252
|
+
}
|
|
253
|
+
fi
|
|
254
|
+
is_pdf "$OUTPUT" || err_json "no PDF was produced at ${OUTPUT}"
|
|
255
|
+
SIZE=$(wc -c < "$OUTPUT" | tr -d ' ')
|
|
256
|
+
jq -cn --arg pdf "$OUTPUT" --argjson size "$SIZE" --argjson parts "${#PARTS[@]}" \
|
|
257
|
+
--arg engines "$(IFS=,; echo "${ENGINES[*]}")" \
|
|
258
|
+
'{success:true, pdf:$pdf, size:$size, parts:$parts, engine:($engines | split(",") | unique | map(select(. != "pdf")) | join(","))}'
|
|
259
|
+
;;
|
|
260
|
+
|
|
261
|
+
merge)
|
|
262
|
+
OUTPUT="$(field .output)"; [ -z "$OUTPUT" ] && err_json "output is required"
|
|
263
|
+
INPUTS=()
|
|
264
|
+
while IFS= read -r p; do [ -n "$p" ] && INPUTS+=("$p"); done < <(printf '%s' "$INPUT" | jq -r '(.inputs // [])[]')
|
|
265
|
+
[ ${#INPUTS[@]} -lt 2 ] && err_json "inputs must list at least two PDFs"
|
|
266
|
+
for p in "${INPUTS[@]}"; do is_pdf "$p" || err_json "not a PDF: ${p}"; done
|
|
267
|
+
mkdir -p "$(dirname "$OUTPUT")"
|
|
268
|
+
run_helper merge "$OUTPUT" "${INPUTS[@]}"
|
|
269
|
+
;;
|
|
270
|
+
|
|
271
|
+
read)
|
|
272
|
+
IN="$(field .input)"; [ -z "$IN" ] && err_json "input (a PDF path) is required"
|
|
273
|
+
is_pdf "$IN" || err_json "not a PDF (or empty): ${IN}"
|
|
274
|
+
ARGS=(read "$IN")
|
|
275
|
+
PAGES="$(field .pages)"; [ -n "$PAGES" ] && ARGS+=(--pages "$PAGES")
|
|
276
|
+
MAXC="$(field .maxChars)"; [ -n "$MAXC" ] && ARGS+=(--max-chars "$MAXC")
|
|
277
|
+
TF="$(field .textFile)"; [ -n "$TF" ] && ARGS+=(--text-file "$TF")
|
|
278
|
+
run_helper "${ARGS[@]}"
|
|
279
|
+
;;
|
|
280
|
+
|
|
281
|
+
info)
|
|
282
|
+
IN="$(field .input)"; [ -z "$IN" ] && err_json "input (a PDF path) is required"
|
|
283
|
+
is_pdf "$IN" || err_json "not a PDF (or empty): ${IN}"
|
|
284
|
+
run_helper info "$IN"
|
|
285
|
+
;;
|
|
286
|
+
|
|
287
|
+
check)
|
|
288
|
+
CHROME="$(find_chrome || true)"
|
|
289
|
+
PY="$(python_bin)"
|
|
290
|
+
jq -cn --arg chrome "$CHROME" --arg python "$PY" \
|
|
291
|
+
--argjson weasy "$(weasy_available && echo true || echo false)" \
|
|
292
|
+
--argjson pypdf "$(py_has pypdf && echo true || echo false)" \
|
|
293
|
+
--argjson markdown "$(py_has markdown && echo true || echo false)" \
|
|
294
|
+
--argjson pdftotext "$(command -v pdftotext >/dev/null 2>&1 && echo true || echo false)" \
|
|
295
|
+
'{success:true, chrome:(if $chrome == "" then null else $chrome end), python:(if $python == "" then null else $python end),
|
|
296
|
+
weasyprint:$weasy, pypdf:$pypdf, markdown:$markdown, pdftotext:$pdftotext,
|
|
297
|
+
canRender:(($chrome != "") or $weasy), canRead:($pypdf or $pdftotext), canMerge:$pypdf}'
|
|
298
|
+
;;
|
|
299
|
+
|
|
300
|
+
*) err_json "unknown action: ${ACTION} (use render, merge, read, info or check)" ;;
|
|
301
|
+
esac
|